configorama 0.9.5 → 0.9.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.
- package/README.md +156 -5
- package/package.json +20 -2
- package/src/main.js +268 -105
- package/src/parsers/esm.js +0 -14
- package/src/parsers/hcl-parse-script.js +40 -0
- package/src/parsers/hcl.js +131 -3
- package/src/parsers/hcl.slow-test.js +141 -0
- package/src/parsers/index.js +3 -1
- package/src/parsers/typescript.js +0 -10
- package/src/resolvers/valueFromEval.js +69 -11
- package/src/resolvers/valueFromFile.js +54 -1
- package/src/resolvers/valueFromIf.js +75 -0
- package/src/resolvers/valueFromIf.test.js +66 -0
- package/src/resolvers/valueFromNumber.js +3 -0
- package/src/utils/handleSignalEvents.js +3 -4
- package/src/utils/lodash.js +18 -7
- package/src/utils/parsing/cloudformationSchema.js +1 -2
- package/src/utils/parsing/cloudformationSchema.test.js +14 -0
- package/src/utils/parsing/parse.js +11 -1
- package/src/utils/parsing/preProcess.js +220 -5
- package/src/utils/paths/getFullFilePath.js +6 -2
- package/src/utils/paths/getFullFilePath.test.js +18 -0
- package/src/utils/regex/index.js +18 -3
- package/src/utils/regex/index.test.js +24 -0
- package/src/utils/strings/quoteAware.js +141 -0
- package/src/utils/strings/replaceAll.js +13 -1
- package/src/utils/strings/splitByComma.js +25 -15
- package/src/utils/strings/splitByComma.test.js +19 -0
- package/src/utils/strings/splitOnPipe.js +30 -0
- package/src/utils/strings/splitOnPipe.test.js +68 -0
- package/src/utils/validation/isValidValue.test.js +1 -1
- package/src/utils/validation/warnIfNotFound.js +1 -1
- package/src/utils/variables/findNestedVariables.js +8 -2
- package/types/src/main.d.ts +3 -1
- package/types/src/main.d.ts.map +1 -1
- package/types/src/parsers/esm.d.ts.map +1 -1
- package/types/src/parsers/hcl-parse-script.d.ts +3 -0
- package/types/src/parsers/hcl-parse-script.d.ts.map +1 -0
- package/types/src/parsers/hcl.d.ts +43 -0
- package/types/src/parsers/hcl.d.ts.map +1 -1
- package/types/src/parsers/hcl.slow-test.d.ts +2 -0
- package/types/src/parsers/hcl.slow-test.d.ts.map +1 -0
- package/types/src/parsers/typescript.d.ts.map +1 -1
- package/types/src/resolvers/valueFromEval.d.ts +1 -0
- package/types/src/resolvers/valueFromEval.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts +4 -0
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromIf.d.ts +7 -0
- package/types/src/resolvers/valueFromIf.d.ts.map +1 -0
- package/types/src/resolvers/valueFromNumber.d.ts.map +1 -1
- package/types/src/utils/handleSignalEvents.d.ts.map +1 -1
- package/types/src/utils/lodash.d.ts.map +1 -1
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/parsing/preProcess.d.ts +5 -1
- package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
- package/types/src/utils/paths/getFullFilePath.d.ts.map +1 -1
- package/types/src/utils/regex/index.d.ts.map +1 -1
- package/types/src/utils/strings/quoteAware.d.ts +30 -0
- package/types/src/utils/strings/quoteAware.d.ts.map +1 -0
- package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
- package/types/src/utils/strings/splitByComma.d.ts +1 -1
- package/types/src/utils/strings/splitByComma.d.ts.map +1 -1
- package/types/src/utils/strings/splitOnPipe.d.ts +8 -0
- package/types/src/utils/strings/splitOnPipe.d.ts.map +1 -0
- package/types/src/utils/variables/findNestedVariables.d.ts.map +1 -1
package/src/main.js
CHANGED
|
@@ -42,6 +42,7 @@ const { splitCsv } = require('./utils/strings/splitCsv')
|
|
|
42
42
|
const { replaceAll } = require('./utils/strings/replaceAll')
|
|
43
43
|
const { getTextAfterOccurrence, findNestedVariable } = require('./utils/strings/textUtils')
|
|
44
44
|
const { ensureQuote, isSurroundedByQuotes, startsWithQuotedPipe } = require('./utils/strings/quoteUtils')
|
|
45
|
+
const { splitOnPipe } = require('./utils/strings/splitOnPipe')
|
|
45
46
|
/* Utils - ui */
|
|
46
47
|
const chalk = require('./utils/ui/chalk')
|
|
47
48
|
const deepLog = require('./utils/ui/deep-log')
|
|
@@ -63,6 +64,8 @@ const getValueFromOptions = require('./resolvers/valueFromOptions')
|
|
|
63
64
|
const getValueFromParam = require('./resolvers/valueFromParam')
|
|
64
65
|
const getValueFromCron = require('./resolvers/valueFromCron')
|
|
65
66
|
const getValueFromEval = require('./resolvers/valueFromEval')
|
|
67
|
+
const { encodeValue: encodeValueForEval } = require('./resolvers/valueFromEval')
|
|
68
|
+
const getValueFromIf = require('./resolvers/valueFromIf')
|
|
66
69
|
const createGitResolver = require('./resolvers/valueFromGit')
|
|
67
70
|
const { getValueFromFile: getValueFromFileResolver } = require('./resolvers/valueFromFile')
|
|
68
71
|
/* Parsers */
|
|
@@ -151,6 +154,8 @@ class Configorama {
|
|
|
151
154
|
this.settings.allowUnresolvedVariables = unresolvedSetting
|
|
152
155
|
|
|
153
156
|
this.filterCache = {}
|
|
157
|
+
// Cache for originalValue lookups (perf: avoid repeated dotProp.get)
|
|
158
|
+
this._originalValueCache = new Map()
|
|
154
159
|
|
|
155
160
|
this.foundVariables = []
|
|
156
161
|
this.fileRefsFound = []
|
|
@@ -158,17 +163,25 @@ class Configorama {
|
|
|
158
163
|
// Track variable resolutions for metadata (keyed by path)
|
|
159
164
|
this.resolutionTracking = {}
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
// Detect file type early to determine default syntax
|
|
167
|
+
let detectedFileType = null
|
|
168
|
+
if (typeof fileOrObject === 'string') {
|
|
169
|
+
detectedFileType = path.extname(fileOrObject).toLowerCase()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Use $[...] syntax for HCL/Terraform files to avoid conflicts with Terraform's ${} syntax
|
|
173
|
+
const isHclFile = detectedFileType === '.tf' || detectedFileType === '.hcl'
|
|
174
|
+
const defaultSyntax = isHclFile
|
|
175
|
+
? buildVariableSyntax('$[', ']', ['AWS', 'stageVariables'])
|
|
176
|
+
: buildVariableSyntax('${', '}', ['AWS', 'stageVariables'])
|
|
162
177
|
|
|
163
178
|
const varSyntax = options.syntax || defaultSyntax
|
|
164
179
|
let varRegex
|
|
165
180
|
if (typeof varSyntax === 'string') {
|
|
166
181
|
varRegex = new RegExp(varSyntax, 'g')
|
|
167
|
-
// this.variableSyntax = /\${((?!AWS)([ ~:a-zA-Z0-9=+!@#%*<>?._'",|\-\/\(\)\\]+?|(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*))}/
|
|
168
182
|
} else if (varSyntax instanceof RegExp) {
|
|
169
183
|
varRegex = varSyntax
|
|
170
184
|
}
|
|
171
|
-
// console.log('varRegex', varRegex)
|
|
172
185
|
const variableSyntax = varRegex
|
|
173
186
|
this.variableSyntax = variableSyntax
|
|
174
187
|
|
|
@@ -183,10 +196,15 @@ class Configorama {
|
|
|
183
196
|
|
|
184
197
|
// Set initial config object to populate
|
|
185
198
|
if (typeof fileOrObject === 'object') {
|
|
199
|
+
// Store truly raw config before any preprocessing
|
|
200
|
+
this.rawOriginalConfig = cloneDeep(fileOrObject)
|
|
201
|
+
// Preprocess: convert bare refs in if(), escape help() args
|
|
202
|
+
// Skip fallback fixing for object configs (they handle bare refs differently)
|
|
203
|
+
const processed = preProcess(fileOrObject, this.variableSyntax, this.variableTypes, { skipFallbackFix: true })
|
|
186
204
|
// set config objects
|
|
187
|
-
this.config =
|
|
205
|
+
this.config = processed
|
|
188
206
|
// Keep a copy
|
|
189
|
-
this.originalConfig = cloneDeep(
|
|
207
|
+
this.originalConfig = cloneDeep(processed)
|
|
190
208
|
// Set configPath for file references
|
|
191
209
|
this.configPath = options.configDir || process.cwd()
|
|
192
210
|
} else if (typeof fileOrObject === 'string') {
|
|
@@ -251,6 +269,13 @@ class Configorama {
|
|
|
251
269
|
*/
|
|
252
270
|
getValueFromEval,
|
|
253
271
|
|
|
272
|
+
/**
|
|
273
|
+
* If expressions (alias for eval)
|
|
274
|
+
* Usage:
|
|
275
|
+
* ${if(${self:value} > 10 ? "big" : "small")}
|
|
276
|
+
*/
|
|
277
|
+
getValueFromIf,
|
|
278
|
+
|
|
254
279
|
/**
|
|
255
280
|
* Self references
|
|
256
281
|
* Usage:
|
|
@@ -395,6 +420,15 @@ class Configorama {
|
|
|
395
420
|
)
|
|
396
421
|
this.variablesKnownTypes = variablesKnownTypes
|
|
397
422
|
|
|
423
|
+
// Build prefix lookup map for O(1) type detection (perf optimization)
|
|
424
|
+
this._resolverByPrefix = new Map()
|
|
425
|
+
for (const r of this.variableTypes) {
|
|
426
|
+
const prefix = r.prefix || r.type
|
|
427
|
+
if (prefix && r.match instanceof RegExp && !r.internal) {
|
|
428
|
+
this._resolverByPrefix.set(prefix + ':', r)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
398
432
|
// this.allPatterns = combineRegexes(...this.variableTypes.map((v) => v.match))
|
|
399
433
|
// console.log('this.allPatterns', this.allPatterns)
|
|
400
434
|
// console.log('this.variablesKnownTypes', this.variablesKnownTypes)
|
|
@@ -584,19 +618,26 @@ class Configorama {
|
|
|
584
618
|
*/
|
|
585
619
|
isUnknownTypeAllowed(varString) {
|
|
586
620
|
const setting = this.settings.allowUnknownVariableTypes
|
|
587
|
-
if (setting === true) return true
|
|
588
621
|
if (setting === false || setting === undefined) return false
|
|
622
|
+
|
|
623
|
+
// Extract type prefix from variable string
|
|
624
|
+
// Handle both 'ssm:path' and '${ssm:path}' formats
|
|
625
|
+
let cleanVar = varString
|
|
626
|
+
if (cleanVar.startsWith(this.varPrefix)) {
|
|
627
|
+
cleanVar = cleanVar.slice(this.varPrefix.length)
|
|
628
|
+
}
|
|
629
|
+
if (cleanVar.endsWith(this.varSuffix)) {
|
|
630
|
+
cleanVar = cleanVar.slice(0, -this.varSuffix.length)
|
|
631
|
+
}
|
|
632
|
+
const typePrefix = this.extractTypePrefix(cleanVar)
|
|
633
|
+
|
|
634
|
+
// Check if this is a known type (has a resolver) - known types should not be treated as "unknown allowed"
|
|
635
|
+
const isKnownType = typePrefix && this._resolverByPrefix && this._resolverByPrefix.has(typePrefix + ':')
|
|
636
|
+
if (isKnownType) return false
|
|
637
|
+
|
|
638
|
+
if (setting === true) return true
|
|
639
|
+
|
|
589
640
|
if (Array.isArray(setting)) {
|
|
590
|
-
// Extract type prefix from variable string
|
|
591
|
-
// Handle both 'ssm:path' and '${ssm:path}' formats
|
|
592
|
-
let cleanVar = varString
|
|
593
|
-
if (cleanVar.startsWith(this.varPrefix)) {
|
|
594
|
-
cleanVar = cleanVar.slice(this.varPrefix.length)
|
|
595
|
-
}
|
|
596
|
-
if (cleanVar.endsWith(this.varSuffix)) {
|
|
597
|
-
cleanVar = cleanVar.slice(0, -this.varSuffix.length)
|
|
598
|
-
}
|
|
599
|
-
const typePrefix = this.extractTypePrefix(cleanVar)
|
|
600
641
|
if (typePrefix && setting.includes(typePrefix)) return true
|
|
601
642
|
}
|
|
602
643
|
return false
|
|
@@ -1234,6 +1275,7 @@ class Configorama {
|
|
|
1234
1275
|
const transform = this.runFunction.bind(this)
|
|
1235
1276
|
const varSyntax = this.variableSyntax
|
|
1236
1277
|
const leaves = this.leaves
|
|
1278
|
+
const filters = this.filters
|
|
1237
1279
|
// console.log('leaves two', leaves)
|
|
1238
1280
|
// Traverse resolved object and run functions
|
|
1239
1281
|
// console.log('this.config', this.config)
|
|
@@ -1268,17 +1310,59 @@ class Configorama {
|
|
|
1268
1310
|
// console.log('funcString', funcString)
|
|
1269
1311
|
const func = cleanVariable(funcString, varSyntax, true, `init ${this.callCount}`)
|
|
1270
1312
|
const funcVal = transform(func)
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1313
|
+
|
|
1314
|
+
// Strip filters like " | toUpperCase" before checking for property/index access
|
|
1315
|
+
const rawValueNoFilters = rawValue.replace(/\s*\|.*$/, '')
|
|
1316
|
+
|
|
1317
|
+
// Helper to get property from value (works on objects, arrays, and primitives)
|
|
1318
|
+
const getProp = (val, path) => {
|
|
1319
|
+
if (val == null) return undefined
|
|
1320
|
+
// For primitives (string, number), access property directly
|
|
1321
|
+
if (typeof val !== 'object') {
|
|
1322
|
+
// Handle single property like 'length'
|
|
1323
|
+
if (!path.includes('.')) return val[path]
|
|
1324
|
+
// Handle path like 'foo.bar' - not applicable for primitives
|
|
1325
|
+
return undefined
|
|
1326
|
+
}
|
|
1327
|
+
return dotProp.get(val, path)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Extract filters from rawValue if present (may end with } from ${...})
|
|
1331
|
+
// Handles multiple filters like "| trim | toUpperCase"
|
|
1332
|
+
const pipeIdx = rawValue.indexOf('|')
|
|
1333
|
+
const filterNames = pipeIdx > -1
|
|
1334
|
+
? splitOnPipe(rawValue.slice(pipeIdx).replace(/\}$/, ''))
|
|
1335
|
+
.map(f => f.trim().split('(')[0])
|
|
1336
|
+
.filter(Boolean)
|
|
1337
|
+
: []
|
|
1338
|
+
|
|
1339
|
+
let finalValue = funcVal
|
|
1340
|
+
|
|
1341
|
+
// Check for array index access: [N] optionally followed by .property
|
|
1342
|
+
const indexMatch = rawValueNoFilters.match(/[)\}]\s*\[(\d+)\](?:\.([\w.]+))?$/)
|
|
1343
|
+
if (indexMatch && Array.isArray(funcVal)) {
|
|
1344
|
+
const index = parseInt(indexMatch[1], 10)
|
|
1345
|
+
const propPath = indexMatch[2]
|
|
1346
|
+
finalValue = funcVal[index]
|
|
1347
|
+
if (propPath && finalValue != null) {
|
|
1348
|
+
finalValue = getProp(finalValue, propPath)
|
|
1349
|
+
}
|
|
1279
1350
|
} else {
|
|
1280
|
-
|
|
1351
|
+
// Check for property access: .foo.bar after function close
|
|
1352
|
+
const propMatch = rawValueNoFilters.match(/[)\}]\s*\.([\w.]+)$/)
|
|
1353
|
+
if (propMatch && typeof funcVal === 'object') {
|
|
1354
|
+
finalValue = dotProp.get(funcVal, propMatch[1])
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Apply filters in sequence
|
|
1359
|
+
for (const filterName of filterNames) {
|
|
1360
|
+
if (filters[filterName]) {
|
|
1361
|
+
finalValue = filters[filterName](finalValue)
|
|
1362
|
+
}
|
|
1281
1363
|
}
|
|
1364
|
+
|
|
1365
|
+
this.update(finalValue)
|
|
1282
1366
|
}
|
|
1283
1367
|
|
|
1284
1368
|
/* fix for file(JS-ref.js, raw) to keep parens and inline code */
|
|
@@ -1375,8 +1459,7 @@ class Configorama {
|
|
|
1375
1459
|
if (hasFilters) {
|
|
1376
1460
|
// Extract filter names from the match (e.g., "| String}" -> ["String"])
|
|
1377
1461
|
const filterPart = hasFilters[0].replace(/}?$/, '') // Remove trailing }
|
|
1378
|
-
foundFilters = filterPart
|
|
1379
|
-
.split('|')
|
|
1462
|
+
foundFilters = splitOnPipe(filterPart)
|
|
1380
1463
|
.map((filter) => filter.trim())
|
|
1381
1464
|
.filter(Boolean)
|
|
1382
1465
|
|
|
@@ -1844,21 +1927,35 @@ class Configorama {
|
|
|
1844
1927
|
const thePath = leaf.path.length > 1 ? leaf.path.join('.') : leaf.path[0]
|
|
1845
1928
|
// console.log('thePath', thePath)
|
|
1846
1929
|
// console.log('this.originalConfig', this.originalConfig)
|
|
1847
|
-
|
|
1848
|
-
//
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1930
|
+
|
|
1931
|
+
// Check cache first (perf: avoid repeated dotProp.get calls)
|
|
1932
|
+
let originalValue
|
|
1933
|
+
let originalValuePath
|
|
1934
|
+
if (this._originalValueCache.has(thePath)) {
|
|
1935
|
+
const cached = this._originalValueCache.get(thePath)
|
|
1936
|
+
originalValue = cached.value
|
|
1937
|
+
originalValuePath = cached.originalValuePath
|
|
1938
|
+
} else {
|
|
1939
|
+
originalValue = dotProp.get(this.originalConfig, thePath)
|
|
1940
|
+
// TODO @DWELLS make recursive
|
|
1941
|
+
if (!originalValue) {
|
|
1942
|
+
// Recurse up the tree until we find a value
|
|
1943
|
+
// Use index instead of slice() to avoid array allocations
|
|
1944
|
+
for (let pathLen = leaf.path.length - 1; pathLen > 0 && !originalValue; pathLen--) {
|
|
1945
|
+
const currentPath = leaf.path.slice(0, pathLen).join('.')
|
|
1946
|
+
// console.log('checking parent path:', currentPath)
|
|
1947
|
+
originalValue = dotProp.get(this.originalConfig, currentPath)
|
|
1948
|
+
if (typeof originalValue !== 'undefined') {
|
|
1949
|
+
originalValuePath = currentPath
|
|
1950
|
+
}
|
|
1859
1951
|
}
|
|
1860
|
-
currentPathArray = currentPathArray.slice(0, -1)
|
|
1861
1952
|
}
|
|
1953
|
+
// Cache the result
|
|
1954
|
+
this._originalValueCache.set(thePath, { value: originalValue, originalValuePath })
|
|
1955
|
+
}
|
|
1956
|
+
if (originalValuePath) {
|
|
1957
|
+
leaf.originalValuePath = originalValuePath
|
|
1958
|
+
leaf.currentConfig = this.config
|
|
1862
1959
|
}
|
|
1863
1960
|
leaf.originalSource = originalValue
|
|
1864
1961
|
|
|
@@ -1995,6 +2092,7 @@ class Configorama {
|
|
|
1995
2092
|
// Initialize resolution history if needed
|
|
1996
2093
|
if (!valueObject.resolutionHistory) {
|
|
1997
2094
|
valueObject.resolutionHistory = []
|
|
2095
|
+
valueObject._historyKeys = new Set()
|
|
1998
2096
|
}
|
|
1999
2097
|
|
|
2000
2098
|
let result = valueObject.value
|
|
@@ -2113,12 +2211,13 @@ class Configorama {
|
|
|
2113
2211
|
}
|
|
2114
2212
|
|
|
2115
2213
|
// Only add to history if not a duplicate (same match + variable)
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
if (!
|
|
2214
|
+
// Use Set for O(1) lookup instead of O(n) array scan
|
|
2215
|
+
const historyKey = `${historyEntry.match}|${historyEntry.variable}`
|
|
2216
|
+
if (!valueObject._historyKeys) {
|
|
2217
|
+
valueObject._historyKeys = new Set()
|
|
2218
|
+
}
|
|
2219
|
+
if (!valueObject._historyKeys.has(historyKey)) {
|
|
2220
|
+
valueObject._historyKeys.add(historyKey)
|
|
2122
2221
|
valueObject.resolutionHistory.push(historyEntry)
|
|
2123
2222
|
}
|
|
2124
2223
|
|
|
@@ -2277,9 +2376,8 @@ class Configorama {
|
|
|
2277
2376
|
const hasFilters = originalSrc.match(this.filterMatch)
|
|
2278
2377
|
let foundFilters = []
|
|
2279
2378
|
if (hasFilters) {
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
.split('|')
|
|
2379
|
+
const filterPart = hasFilters[0].replace(this.varSuffixPattern, '')
|
|
2380
|
+
foundFilters = splitOnPipe(filterPart)
|
|
2283
2381
|
.map((filter) => filter.trim())
|
|
2284
2382
|
.filter(Boolean)
|
|
2285
2383
|
}
|
|
@@ -2325,7 +2423,9 @@ class Configorama {
|
|
|
2325
2423
|
let deepIndex = Number(v.match(deepIndexPattern)[1])
|
|
2326
2424
|
let item = this.deep[deepIndex]
|
|
2327
2425
|
|
|
2328
|
-
if
|
|
2426
|
+
// Only follow chain if item IS a deep ref (not just contains one)
|
|
2427
|
+
// e.g. item = "${deep:0}" should follow, but item = "https://...${deep:0}..." should not
|
|
2428
|
+
if (/^\$\{deep:\d+\}$/.test(item)) {
|
|
2329
2429
|
deepIndex = Number(item.match(deepIndexPattern)[1])
|
|
2330
2430
|
item = this.deep[deepIndex]
|
|
2331
2431
|
}
|
|
@@ -2372,6 +2472,18 @@ class Configorama {
|
|
|
2372
2472
|
valueToPopulate = valueToPopulate.replace(this.varSuffixPattern, '')
|
|
2373
2473
|
}
|
|
2374
2474
|
|
|
2475
|
+
// For eval/if expressions, string values need quotes unless already quoted
|
|
2476
|
+
// BUT don't quote strings that contain variable refs (they need further resolution)
|
|
2477
|
+
if (/\b(eval|if)\s*\(/.test(property) && !valueToPopulate.match(this.variableSyntax)) {
|
|
2478
|
+
const matchIdx = property.indexOf(currentMatchedString)
|
|
2479
|
+
const charBefore = matchIdx > 0 ? property[matchIdx - 1] : ''
|
|
2480
|
+
// Always escape quotes in values for eval/if context
|
|
2481
|
+
valueToPopulate = valueToPopulate.replace(/"/g, '\\"')
|
|
2482
|
+
if (charBefore !== '"' && charBefore !== "'") {
|
|
2483
|
+
// Not already quoted, wrap in quotes for eval
|
|
2484
|
+
valueToPopulate = `"${valueToPopulate}"`
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2375
2487
|
property = replaceAll(currentMatchedString, valueToPopulate, property)
|
|
2376
2488
|
// console.log('property replaceAll', property)
|
|
2377
2489
|
|
|
@@ -2387,37 +2499,55 @@ class Configorama {
|
|
|
2387
2499
|
// } else if (isArray(valueToPopulate) && valueToPopulate.length === 1) {
|
|
2388
2500
|
// property = replaceAll(matchedString, String(valueToPopulate[0]), property)
|
|
2389
2501
|
} else if (isObject(valueToPopulate)) {
|
|
2390
|
-
if (DEBUG_TYPE) console.log('
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
property
|
|
2397
|
-
matchedString.match(this.variableSyntax) &&
|
|
2398
|
-
property.match(this.variableSyntax)
|
|
2399
|
-
)
|
|
2400
|
-
// Only encode for file() or text() references where JSON braces break regex matching
|
|
2401
|
-
const isFileOrTextRef = /\bfile\s*\(|\btext\s*\(/.test(property)
|
|
2402
|
-
if (isNestedInVariable && isFileOrTextRef) {
|
|
2403
|
-
// Encode object as base64 to avoid breaking variable syntax with nested braces
|
|
2404
|
-
const encodedObj = encodeJsonForVariable(valueToPopulate)
|
|
2405
|
-
property = replaceAll(matchedString, encodedObj, property)
|
|
2406
|
-
} else if (isNestedInVariable) {
|
|
2407
|
-
const isVar = /^\${[a-zA-Z0-9_]+:/.test(property)
|
|
2408
|
-
if (isVar) {
|
|
2409
|
-
throw new Error(
|
|
2410
|
-
`Invalid variable syntax "${property}" resolves to "${replaceAll(matchedString, objStr, property)}"`,
|
|
2411
|
-
)
|
|
2412
|
-
}
|
|
2413
|
-
property = replaceAll(matchedString, objStr, property)
|
|
2502
|
+
if (DEBUG_TYPE) console.log('DEBUG_TYPE isObject')
|
|
2503
|
+
|
|
2504
|
+
// For eval/if expressions, encode objects to avoid {} breaking variable syntax
|
|
2505
|
+
const isEvalOrIf = /\b(eval|if)\s*\(/.test(property)
|
|
2506
|
+
if (isEvalOrIf) {
|
|
2507
|
+
const encoded = encodeValueForEval(valueToPopulate)
|
|
2508
|
+
property = replaceAll(matchedString, encoded, property)
|
|
2414
2509
|
} else {
|
|
2415
|
-
|
|
2416
|
-
|
|
2510
|
+
const objStr = JSON.stringify(valueToPopulate)
|
|
2511
|
+
/* Check if variable inside another variable. E.g. ${env:${self:someObject}} that resolves to ${env:{...}} */
|
|
2512
|
+
const isNestedInVariable = (
|
|
2513
|
+
property.trim() !== matchedString.trim() &&
|
|
2514
|
+
property.indexOf(matchedString) !== -1 &&
|
|
2515
|
+
matchedString.match(this.variableSyntax) &&
|
|
2516
|
+
property.match(this.variableSyntax)
|
|
2517
|
+
)
|
|
2518
|
+
// Only encode for file() or text() references where JSON braces break regex matching
|
|
2519
|
+
const isFileOrTextRef = /\bfile\s*\(|\btext\s*\(/.test(property)
|
|
2520
|
+
if (isNestedInVariable && isFileOrTextRef) {
|
|
2521
|
+
// Encode object as base64 to avoid breaking variable syntax with nested braces
|
|
2522
|
+
const encodedObj = encodeJsonForVariable(valueToPopulate)
|
|
2523
|
+
property = replaceAll(matchedString, encodedObj, property)
|
|
2524
|
+
} else if (isNestedInVariable) {
|
|
2525
|
+
const isVar = /^\${[a-zA-Z0-9_]+:/.test(property)
|
|
2526
|
+
if (isVar) {
|
|
2527
|
+
throw new Error(
|
|
2528
|
+
`Invalid variable syntax "${property}" resolves to "${replaceAll(matchedString, objStr, property)}"`,
|
|
2529
|
+
)
|
|
2530
|
+
}
|
|
2531
|
+
property = replaceAll(matchedString, objStr, property)
|
|
2532
|
+
} else {
|
|
2533
|
+
// console.log('OBJECT MATCH', `"${objStr}"`)
|
|
2534
|
+
property = replaceAll(matchedString, objStr, property)
|
|
2535
|
+
}
|
|
2417
2536
|
}
|
|
2418
2537
|
// console.log('property', property)
|
|
2419
2538
|
// TODO run functions here
|
|
2420
2539
|
// console.log('other new prop', property)
|
|
2540
|
+
|
|
2541
|
+
// partial replacement, boolean inside eval/if expressions
|
|
2542
|
+
} else if (typeof valueToPopulate === 'boolean' && /\b(eval|if)\s*\(/.test(property)) {
|
|
2543
|
+
if (DEBUG_TYPE) console.log('DEBUG_TYPE isBoolean in eval/if')
|
|
2544
|
+
property = replaceAll(matchedString, String(valueToPopulate), property)
|
|
2545
|
+
|
|
2546
|
+
// partial replacement, null inside eval/if expressions
|
|
2547
|
+
} else if (valueToPopulate === null && /\b(eval|if)\s*\(/.test(property)) {
|
|
2548
|
+
if (DEBUG_TYPE) console.log('DEBUG_TYPE isNull in eval/if')
|
|
2549
|
+
property = replaceAll(matchedString, '__NULL__', property)
|
|
2550
|
+
|
|
2421
2551
|
} else {
|
|
2422
2552
|
if (DEBUG_TYPE) console.log('DEBUG_TYPE else')
|
|
2423
2553
|
let missingValue = matchedString
|
|
@@ -2434,7 +2564,7 @@ class Configorama {
|
|
|
2434
2564
|
true,
|
|
2435
2565
|
`populateVariable fallback ${this.callCount}`
|
|
2436
2566
|
)
|
|
2437
|
-
const cleanVarNoFilters = cleanVar
|
|
2567
|
+
const cleanVarNoFilters = splitOnPipe(cleanVar)[0]
|
|
2438
2568
|
const splitVars = splitByComma(cleanVarNoFilters)
|
|
2439
2569
|
const nestedVar = findNestedVariable(splitVars, valueObject.originalSource)
|
|
2440
2570
|
|
|
@@ -2559,8 +2689,9 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2559
2689
|
/* Not file or text refs */
|
|
2560
2690
|
!prop.match(fileRefSyntax)
|
|
2561
2691
|
&& !prop.match(textRefSyntax)
|
|
2562
|
-
/* Not eval refs */
|
|
2563
|
-
&& !prop.match(getValueFromEval.match)
|
|
2692
|
+
/* Not eval/if refs */
|
|
2693
|
+
&& !prop.match(getValueFromEval.match)
|
|
2694
|
+
&& !prop.match(getValueFromIf.match)
|
|
2564
2695
|
// AND is not multiline value
|
|
2565
2696
|
&& (func && prop.split('\n').length < 3)) {
|
|
2566
2697
|
// console.log('IS FUNCTION')
|
|
@@ -2821,12 +2952,11 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2821
2952
|
promiseKey = deeperValue.match(/\s\|/) ? deeperValue : undefined
|
|
2822
2953
|
|
|
2823
2954
|
// TODO clean this up
|
|
2824
|
-
const t = variableString
|
|
2955
|
+
const t = splitOnPipe(variableString)
|
|
2825
2956
|
// console.log('variableString', variableString)
|
|
2826
2957
|
// console.log('valueObject', valueObject)
|
|
2827
2958
|
// console.log('t', t)
|
|
2828
|
-
const _filter = string
|
|
2829
|
-
.split('|')
|
|
2959
|
+
const _filter = splitOnPipe(string)
|
|
2830
2960
|
.filter((value, index, arr) => {
|
|
2831
2961
|
return index > 0
|
|
2832
2962
|
})
|
|
@@ -2849,24 +2979,40 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2849
2979
|
/** @type {Function|undefined} */
|
|
2850
2980
|
let resolverFunction
|
|
2851
2981
|
let resolverType
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2982
|
+
let found = false
|
|
2983
|
+
|
|
2984
|
+
// Fast path: try prefix lookup first for O(1) detection of common types
|
|
2985
|
+
const colonIdx = variableString.indexOf(':')
|
|
2986
|
+
if (colonIdx !== -1) {
|
|
2987
|
+
const prefix = variableString.slice(0, colonIdx + 1)
|
|
2988
|
+
const resolver = this._resolverByPrefix.get(prefix)
|
|
2989
|
+
if (resolver && resolver.match instanceof RegExp && variableString.match(resolver.match)) {
|
|
2990
|
+
resolverFunction = resolver.resolver
|
|
2991
|
+
resolverType = resolver.type || 'unknown'
|
|
2992
|
+
found = true
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
// Fallback: loop over all variable types
|
|
2997
|
+
if (!found) {
|
|
2998
|
+
found = this.variableTypes.some((r, i) => {
|
|
2999
|
+
if (r.match instanceof RegExp && variableString.match(r.match)) {
|
|
2862
3000
|
// set resolver function
|
|
2863
3001
|
resolverFunction = r.resolver
|
|
2864
3002
|
resolverType = r.type || 'unknown'
|
|
2865
3003
|
return true
|
|
3004
|
+
} else if (typeof r.match === 'function') {
|
|
3005
|
+
// TODO finalize match API
|
|
3006
|
+
if (r.match(variableString, this.config, valueObject)) {
|
|
3007
|
+
// set resolver function
|
|
3008
|
+
resolverFunction = r.resolver
|
|
3009
|
+
resolverType = r.type || 'unknown'
|
|
3010
|
+
return true
|
|
3011
|
+
}
|
|
2866
3012
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
}
|
|
3013
|
+
return false
|
|
3014
|
+
})
|
|
3015
|
+
}
|
|
2870
3016
|
/*
|
|
2871
3017
|
// console.log('found variable resolver', found)
|
|
2872
3018
|
// console.log('resolverFunction', resolverFunction)
|
|
@@ -2907,8 +3053,10 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2907
3053
|
}
|
|
2908
3054
|
|
|
2909
3055
|
// console.log('VALUE', val)
|
|
3056
|
+
// For eval/if resolvers, null is a valid intentional result (e.g., ternary false branch)
|
|
3057
|
+
const isEvalOrIfResolver = resolverType === 'eval' || resolverType === 'if'
|
|
2910
3058
|
if (
|
|
2911
|
-
val === null ||
|
|
3059
|
+
(val === null && !isEvalOrIfResolver) ||
|
|
2912
3060
|
typeof val === 'undefined' ||
|
|
2913
3061
|
/* match deep refs as empty {}, they need resolving via functions */
|
|
2914
3062
|
(typeof val === 'object' && isEmpty(val) && variableString.match(/deep\:/))
|
|
@@ -2966,7 +3114,8 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2966
3114
|
return Promise.resolve(undefined)
|
|
2967
3115
|
}
|
|
2968
3116
|
}
|
|
2969
|
-
|
|
3117
|
+
// Encode only the unknown variable, not the entire string
|
|
3118
|
+
return Promise.resolve(encodeUnknown(this.varPrefix + variableString + this.varSuffix))
|
|
2970
3119
|
}
|
|
2971
3120
|
|
|
2972
3121
|
if (valueCount.length === 1 && noNestedVars) {
|
|
@@ -3040,9 +3189,8 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3040
3189
|
|
|
3041
3190
|
if (typeof val === 'string' && val.match(/deep:/)) {
|
|
3042
3191
|
// TODO refactor the deep filter logic here. match | filter | filter..
|
|
3043
|
-
const
|
|
3044
|
-
|
|
3045
|
-
.split('|')
|
|
3192
|
+
const propWithoutSuffix = propertyString.replace(this.varSuffixPattern, '')
|
|
3193
|
+
const allFilters = splitOnPipe(propWithoutSuffix)
|
|
3046
3194
|
.reduce((acc, currentFilter, i) => {
|
|
3047
3195
|
if (i === 0) {
|
|
3048
3196
|
return acc
|
|
@@ -3108,7 +3256,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3108
3256
|
// TODO @DWELLS cleanVariable makes fallback values with spaces have no spaces
|
|
3109
3257
|
// console.log('AFTER cleanVariable', clean)
|
|
3110
3258
|
// console.log(typeof clean)
|
|
3111
|
-
const cleanClean = clean
|
|
3259
|
+
const cleanClean = splitOnPipe(clean)[0]
|
|
3112
3260
|
// console.log('cleanCleanVariable', cleanClean)
|
|
3113
3261
|
if (funcRegex.exec(cleanClean)) {
|
|
3114
3262
|
const valuePromise = Promise.resolve(cleanClean)
|
|
@@ -3238,10 +3386,13 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3238
3386
|
// console.log('allowUnknownVars propertyString', propertyString)
|
|
3239
3387
|
const varMatches = propertyString.match(this.variableSyntax)
|
|
3240
3388
|
let allowUnknownVars = propertyString
|
|
3241
|
-
/*
|
|
3389
|
+
/* Only encode variables that are actually unknown, not all of them */
|
|
3242
3390
|
if (varMatches && varMatches.length) {
|
|
3243
3391
|
varMatches.forEach((m) => {
|
|
3244
|
-
|
|
3392
|
+
// Only encode this variable if IT is unknown (not just because the string contains unknowns)
|
|
3393
|
+
if (this.isUnknownTypeAllowed(m)) {
|
|
3394
|
+
allowUnknownVars = allowUnknownVars.replace(m, encodeUnknown(m))
|
|
3395
|
+
}
|
|
3245
3396
|
})
|
|
3246
3397
|
}
|
|
3247
3398
|
// console.log('allowUnknownVars propertyString:', propertyString)
|
|
@@ -3298,7 +3449,9 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3298
3449
|
config: this.config,
|
|
3299
3450
|
getDeeperValue: this.getDeeperValue.bind(this),
|
|
3300
3451
|
fileRefSyntax: fileRefSyntax,
|
|
3301
|
-
textRefSyntax: textRefSyntax
|
|
3452
|
+
textRefSyntax: textRefSyntax,
|
|
3453
|
+
varPrefix: this.varPrefix,
|
|
3454
|
+
varSuffix: this.varSuffix
|
|
3302
3455
|
}
|
|
3303
3456
|
return getValueFromFileResolver(ctx, variableString, options)
|
|
3304
3457
|
}
|
|
@@ -3460,7 +3613,17 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3460
3613
|
var hasFunc = funcRegex.exec(variableString)
|
|
3461
3614
|
// TODO finish Function handling. Need to move this down below resolver to resolve inner refs first
|
|
3462
3615
|
// console.log('hasFunc', hasFunc)
|
|
3463
|
-
|
|
3616
|
+
// Skip special expressions (cron, eval, if) - these aren't user functions
|
|
3617
|
+
if (!hasFunc || hasFunc && (hasFunc[1] === 'cron' || hasFunc[1] === 'eval' || hasFunc[1] === 'if')) {
|
|
3618
|
+
return variableString
|
|
3619
|
+
}
|
|
3620
|
+
// Skip file/text when they match resolver regex OR contain encoded passthrough values
|
|
3621
|
+
// Malformed patterns (with %, \, etc) should still error
|
|
3622
|
+
const hasPassthrough = variableString.includes('>passthrough')
|
|
3623
|
+
if (hasFunc[1] === 'file' && (variableString.match(fileRefSyntax) || hasPassthrough)) {
|
|
3624
|
+
return variableString
|
|
3625
|
+
}
|
|
3626
|
+
if (hasFunc[1] === 'text' && (variableString.match(textRefSyntax) || hasPassthrough)) {
|
|
3464
3627
|
return variableString
|
|
3465
3628
|
}
|
|
3466
3629
|
// test for object
|
package/src/parsers/esm.js
CHANGED
|
@@ -19,13 +19,6 @@ async function executeESMFile(filePath, opts = {}) {
|
|
|
19
19
|
const resolvedPath = path.resolve(filePath)
|
|
20
20
|
let esmModule = jiti(resolvedPath)
|
|
21
21
|
|
|
22
|
-
// Handle different export patterns - jiti returns { default: Function } for ESM default exports
|
|
23
|
-
if (esmModule && typeof esmModule === 'object' && esmModule.default) {
|
|
24
|
-
esmModule = esmModule.default
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// For ESM files, we just return the module (object or function)
|
|
28
|
-
// The calling code will determine whether to execute it or not
|
|
29
22
|
return esmModule
|
|
30
23
|
} catch (err) {
|
|
31
24
|
throw new Error(`Failed to load ESM file ${filePath}: ${err.message}`)
|
|
@@ -50,13 +43,6 @@ function executeESMFileSync(filePath, opts = {}) {
|
|
|
50
43
|
const resolvedPath = path.resolve(filePath)
|
|
51
44
|
let esmModule = jiti(resolvedPath)
|
|
52
45
|
|
|
53
|
-
// Handle different export patterns - jiti returns { default: Function } for ESM default exports
|
|
54
|
-
if (esmModule && typeof esmModule === 'object' && esmModule.default) {
|
|
55
|
-
esmModule = esmModule.default
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// For ESM files, we just return the module (object or function)
|
|
59
|
-
// The calling code will determine whether to execute it or not
|
|
60
46
|
return esmModule
|
|
61
47
|
} catch (err) {
|
|
62
48
|
throw new Error(`Failed to load ESM file ${filePath}: ${err.message}`)
|