configorama 0.9.8 → 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 +83 -0
- package/package.json +1 -1
- package/src/main.js +254 -101
- package/src/parsers/esm.js +0 -14
- package/src/parsers/typescript.js +0 -10
- package/src/resolvers/valueFromEval.js +69 -11
- package/src/resolvers/valueFromFile.js +1 -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/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/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/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/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/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 = []
|
|
@@ -191,10 +196,15 @@ class Configorama {
|
|
|
191
196
|
|
|
192
197
|
// Set initial config object to populate
|
|
193
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 })
|
|
194
204
|
// set config objects
|
|
195
|
-
this.config =
|
|
205
|
+
this.config = processed
|
|
196
206
|
// Keep a copy
|
|
197
|
-
this.originalConfig = cloneDeep(
|
|
207
|
+
this.originalConfig = cloneDeep(processed)
|
|
198
208
|
// Set configPath for file references
|
|
199
209
|
this.configPath = options.configDir || process.cwd()
|
|
200
210
|
} else if (typeof fileOrObject === 'string') {
|
|
@@ -259,6 +269,13 @@ class Configorama {
|
|
|
259
269
|
*/
|
|
260
270
|
getValueFromEval,
|
|
261
271
|
|
|
272
|
+
/**
|
|
273
|
+
* If expressions (alias for eval)
|
|
274
|
+
* Usage:
|
|
275
|
+
* ${if(${self:value} > 10 ? "big" : "small")}
|
|
276
|
+
*/
|
|
277
|
+
getValueFromIf,
|
|
278
|
+
|
|
262
279
|
/**
|
|
263
280
|
* Self references
|
|
264
281
|
* Usage:
|
|
@@ -403,6 +420,15 @@ class Configorama {
|
|
|
403
420
|
)
|
|
404
421
|
this.variablesKnownTypes = variablesKnownTypes
|
|
405
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
|
+
|
|
406
432
|
// this.allPatterns = combineRegexes(...this.variableTypes.map((v) => v.match))
|
|
407
433
|
// console.log('this.allPatterns', this.allPatterns)
|
|
408
434
|
// console.log('this.variablesKnownTypes', this.variablesKnownTypes)
|
|
@@ -592,19 +618,26 @@ class Configorama {
|
|
|
592
618
|
*/
|
|
593
619
|
isUnknownTypeAllowed(varString) {
|
|
594
620
|
const setting = this.settings.allowUnknownVariableTypes
|
|
595
|
-
if (setting === true) return true
|
|
596
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
|
+
|
|
597
640
|
if (Array.isArray(setting)) {
|
|
598
|
-
// Extract type prefix from variable string
|
|
599
|
-
// Handle both 'ssm:path' and '${ssm:path}' formats
|
|
600
|
-
let cleanVar = varString
|
|
601
|
-
if (cleanVar.startsWith(this.varPrefix)) {
|
|
602
|
-
cleanVar = cleanVar.slice(this.varPrefix.length)
|
|
603
|
-
}
|
|
604
|
-
if (cleanVar.endsWith(this.varSuffix)) {
|
|
605
|
-
cleanVar = cleanVar.slice(0, -this.varSuffix.length)
|
|
606
|
-
}
|
|
607
|
-
const typePrefix = this.extractTypePrefix(cleanVar)
|
|
608
641
|
if (typePrefix && setting.includes(typePrefix)) return true
|
|
609
642
|
}
|
|
610
643
|
return false
|
|
@@ -1242,6 +1275,7 @@ class Configorama {
|
|
|
1242
1275
|
const transform = this.runFunction.bind(this)
|
|
1243
1276
|
const varSyntax = this.variableSyntax
|
|
1244
1277
|
const leaves = this.leaves
|
|
1278
|
+
const filters = this.filters
|
|
1245
1279
|
// console.log('leaves two', leaves)
|
|
1246
1280
|
// Traverse resolved object and run functions
|
|
1247
1281
|
// console.log('this.config', this.config)
|
|
@@ -1276,17 +1310,59 @@ class Configorama {
|
|
|
1276
1310
|
// console.log('funcString', funcString)
|
|
1277
1311
|
const func = cleanVariable(funcString, varSyntax, true, `init ${this.callCount}`)
|
|
1278
1312
|
const funcVal = transform(func)
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
+
}
|
|
1287
1350
|
} else {
|
|
1288
|
-
|
|
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
|
+
}
|
|
1289
1363
|
}
|
|
1364
|
+
|
|
1365
|
+
this.update(finalValue)
|
|
1290
1366
|
}
|
|
1291
1367
|
|
|
1292
1368
|
/* fix for file(JS-ref.js, raw) to keep parens and inline code */
|
|
@@ -1383,8 +1459,7 @@ class Configorama {
|
|
|
1383
1459
|
if (hasFilters) {
|
|
1384
1460
|
// Extract filter names from the match (e.g., "| String}" -> ["String"])
|
|
1385
1461
|
const filterPart = hasFilters[0].replace(/}?$/, '') // Remove trailing }
|
|
1386
|
-
foundFilters = filterPart
|
|
1387
|
-
.split('|')
|
|
1462
|
+
foundFilters = splitOnPipe(filterPart)
|
|
1388
1463
|
.map((filter) => filter.trim())
|
|
1389
1464
|
.filter(Boolean)
|
|
1390
1465
|
|
|
@@ -1852,21 +1927,35 @@ class Configorama {
|
|
|
1852
1927
|
const thePath = leaf.path.length > 1 ? leaf.path.join('.') : leaf.path[0]
|
|
1853
1928
|
// console.log('thePath', thePath)
|
|
1854
1929
|
// console.log('this.originalConfig', this.originalConfig)
|
|
1855
|
-
|
|
1856
|
-
//
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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
|
+
}
|
|
1867
1951
|
}
|
|
1868
|
-
currentPathArray = currentPathArray.slice(0, -1)
|
|
1869
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
|
|
1870
1959
|
}
|
|
1871
1960
|
leaf.originalSource = originalValue
|
|
1872
1961
|
|
|
@@ -2003,6 +2092,7 @@ class Configorama {
|
|
|
2003
2092
|
// Initialize resolution history if needed
|
|
2004
2093
|
if (!valueObject.resolutionHistory) {
|
|
2005
2094
|
valueObject.resolutionHistory = []
|
|
2095
|
+
valueObject._historyKeys = new Set()
|
|
2006
2096
|
}
|
|
2007
2097
|
|
|
2008
2098
|
let result = valueObject.value
|
|
@@ -2121,12 +2211,13 @@ class Configorama {
|
|
|
2121
2211
|
}
|
|
2122
2212
|
|
|
2123
2213
|
// Only add to history if not a duplicate (same match + variable)
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
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)
|
|
2130
2221
|
valueObject.resolutionHistory.push(historyEntry)
|
|
2131
2222
|
}
|
|
2132
2223
|
|
|
@@ -2285,9 +2376,8 @@ class Configorama {
|
|
|
2285
2376
|
const hasFilters = originalSrc.match(this.filterMatch)
|
|
2286
2377
|
let foundFilters = []
|
|
2287
2378
|
if (hasFilters) {
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
.split('|')
|
|
2379
|
+
const filterPart = hasFilters[0].replace(this.varSuffixPattern, '')
|
|
2380
|
+
foundFilters = splitOnPipe(filterPart)
|
|
2291
2381
|
.map((filter) => filter.trim())
|
|
2292
2382
|
.filter(Boolean)
|
|
2293
2383
|
}
|
|
@@ -2333,7 +2423,9 @@ class Configorama {
|
|
|
2333
2423
|
let deepIndex = Number(v.match(deepIndexPattern)[1])
|
|
2334
2424
|
let item = this.deep[deepIndex]
|
|
2335
2425
|
|
|
2336
|
-
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)) {
|
|
2337
2429
|
deepIndex = Number(item.match(deepIndexPattern)[1])
|
|
2338
2430
|
item = this.deep[deepIndex]
|
|
2339
2431
|
}
|
|
@@ -2380,6 +2472,18 @@ class Configorama {
|
|
|
2380
2472
|
valueToPopulate = valueToPopulate.replace(this.varSuffixPattern, '')
|
|
2381
2473
|
}
|
|
2382
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
|
+
}
|
|
2383
2487
|
property = replaceAll(currentMatchedString, valueToPopulate, property)
|
|
2384
2488
|
// console.log('property replaceAll', property)
|
|
2385
2489
|
|
|
@@ -2395,37 +2499,55 @@ class Configorama {
|
|
|
2395
2499
|
// } else if (isArray(valueToPopulate) && valueToPopulate.length === 1) {
|
|
2396
2500
|
// property = replaceAll(matchedString, String(valueToPopulate[0]), property)
|
|
2397
2501
|
} else if (isObject(valueToPopulate)) {
|
|
2398
|
-
if (DEBUG_TYPE) console.log('
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
property
|
|
2405
|
-
matchedString.match(this.variableSyntax) &&
|
|
2406
|
-
property.match(this.variableSyntax)
|
|
2407
|
-
)
|
|
2408
|
-
// Only encode for file() or text() references where JSON braces break regex matching
|
|
2409
|
-
const isFileOrTextRef = /\bfile\s*\(|\btext\s*\(/.test(property)
|
|
2410
|
-
if (isNestedInVariable && isFileOrTextRef) {
|
|
2411
|
-
// Encode object as base64 to avoid breaking variable syntax with nested braces
|
|
2412
|
-
const encodedObj = encodeJsonForVariable(valueToPopulate)
|
|
2413
|
-
property = replaceAll(matchedString, encodedObj, property)
|
|
2414
|
-
} else if (isNestedInVariable) {
|
|
2415
|
-
const isVar = /^\${[a-zA-Z0-9_]+:/.test(property)
|
|
2416
|
-
if (isVar) {
|
|
2417
|
-
throw new Error(
|
|
2418
|
-
`Invalid variable syntax "${property}" resolves to "${replaceAll(matchedString, objStr, property)}"`,
|
|
2419
|
-
)
|
|
2420
|
-
}
|
|
2421
|
-
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)
|
|
2422
2509
|
} else {
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
+
}
|
|
2425
2536
|
}
|
|
2426
2537
|
// console.log('property', property)
|
|
2427
2538
|
// TODO run functions here
|
|
2428
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
|
+
|
|
2429
2551
|
} else {
|
|
2430
2552
|
if (DEBUG_TYPE) console.log('DEBUG_TYPE else')
|
|
2431
2553
|
let missingValue = matchedString
|
|
@@ -2442,7 +2564,7 @@ class Configorama {
|
|
|
2442
2564
|
true,
|
|
2443
2565
|
`populateVariable fallback ${this.callCount}`
|
|
2444
2566
|
)
|
|
2445
|
-
const cleanVarNoFilters = cleanVar
|
|
2567
|
+
const cleanVarNoFilters = splitOnPipe(cleanVar)[0]
|
|
2446
2568
|
const splitVars = splitByComma(cleanVarNoFilters)
|
|
2447
2569
|
const nestedVar = findNestedVariable(splitVars, valueObject.originalSource)
|
|
2448
2570
|
|
|
@@ -2567,8 +2689,9 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2567
2689
|
/* Not file or text refs */
|
|
2568
2690
|
!prop.match(fileRefSyntax)
|
|
2569
2691
|
&& !prop.match(textRefSyntax)
|
|
2570
|
-
/* Not eval refs */
|
|
2571
|
-
&& !prop.match(getValueFromEval.match)
|
|
2692
|
+
/* Not eval/if refs */
|
|
2693
|
+
&& !prop.match(getValueFromEval.match)
|
|
2694
|
+
&& !prop.match(getValueFromIf.match)
|
|
2572
2695
|
// AND is not multiline value
|
|
2573
2696
|
&& (func && prop.split('\n').length < 3)) {
|
|
2574
2697
|
// console.log('IS FUNCTION')
|
|
@@ -2829,12 +2952,11 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2829
2952
|
promiseKey = deeperValue.match(/\s\|/) ? deeperValue : undefined
|
|
2830
2953
|
|
|
2831
2954
|
// TODO clean this up
|
|
2832
|
-
const t = variableString
|
|
2955
|
+
const t = splitOnPipe(variableString)
|
|
2833
2956
|
// console.log('variableString', variableString)
|
|
2834
2957
|
// console.log('valueObject', valueObject)
|
|
2835
2958
|
// console.log('t', t)
|
|
2836
|
-
const _filter = string
|
|
2837
|
-
.split('|')
|
|
2959
|
+
const _filter = splitOnPipe(string)
|
|
2838
2960
|
.filter((value, index, arr) => {
|
|
2839
2961
|
return index > 0
|
|
2840
2962
|
})
|
|
@@ -2857,24 +2979,40 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2857
2979
|
/** @type {Function|undefined} */
|
|
2858
2980
|
let resolverFunction
|
|
2859
2981
|
let resolverType
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
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)) {
|
|
2870
3000
|
// set resolver function
|
|
2871
3001
|
resolverFunction = r.resolver
|
|
2872
3002
|
resolverType = r.type || 'unknown'
|
|
2873
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
|
+
}
|
|
2874
3012
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
}
|
|
3013
|
+
return false
|
|
3014
|
+
})
|
|
3015
|
+
}
|
|
2878
3016
|
/*
|
|
2879
3017
|
// console.log('found variable resolver', found)
|
|
2880
3018
|
// console.log('resolverFunction', resolverFunction)
|
|
@@ -2915,8 +3053,10 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2915
3053
|
}
|
|
2916
3054
|
|
|
2917
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'
|
|
2918
3058
|
if (
|
|
2919
|
-
val === null ||
|
|
3059
|
+
(val === null && !isEvalOrIfResolver) ||
|
|
2920
3060
|
typeof val === 'undefined' ||
|
|
2921
3061
|
/* match deep refs as empty {}, they need resolving via functions */
|
|
2922
3062
|
(typeof val === 'object' && isEmpty(val) && variableString.match(/deep\:/))
|
|
@@ -2974,7 +3114,8 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2974
3114
|
return Promise.resolve(undefined)
|
|
2975
3115
|
}
|
|
2976
3116
|
}
|
|
2977
|
-
|
|
3117
|
+
// Encode only the unknown variable, not the entire string
|
|
3118
|
+
return Promise.resolve(encodeUnknown(this.varPrefix + variableString + this.varSuffix))
|
|
2978
3119
|
}
|
|
2979
3120
|
|
|
2980
3121
|
if (valueCount.length === 1 && noNestedVars) {
|
|
@@ -3048,9 +3189,8 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3048
3189
|
|
|
3049
3190
|
if (typeof val === 'string' && val.match(/deep:/)) {
|
|
3050
3191
|
// TODO refactor the deep filter logic here. match | filter | filter..
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3053
|
-
.split('|')
|
|
3192
|
+
const propWithoutSuffix = propertyString.replace(this.varSuffixPattern, '')
|
|
3193
|
+
const allFilters = splitOnPipe(propWithoutSuffix)
|
|
3054
3194
|
.reduce((acc, currentFilter, i) => {
|
|
3055
3195
|
if (i === 0) {
|
|
3056
3196
|
return acc
|
|
@@ -3116,7 +3256,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3116
3256
|
// TODO @DWELLS cleanVariable makes fallback values with spaces have no spaces
|
|
3117
3257
|
// console.log('AFTER cleanVariable', clean)
|
|
3118
3258
|
// console.log(typeof clean)
|
|
3119
|
-
const cleanClean = clean
|
|
3259
|
+
const cleanClean = splitOnPipe(clean)[0]
|
|
3120
3260
|
// console.log('cleanCleanVariable', cleanClean)
|
|
3121
3261
|
if (funcRegex.exec(cleanClean)) {
|
|
3122
3262
|
const valuePromise = Promise.resolve(cleanClean)
|
|
@@ -3246,10 +3386,13 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3246
3386
|
// console.log('allowUnknownVars propertyString', propertyString)
|
|
3247
3387
|
const varMatches = propertyString.match(this.variableSyntax)
|
|
3248
3388
|
let allowUnknownVars = propertyString
|
|
3249
|
-
/*
|
|
3389
|
+
/* Only encode variables that are actually unknown, not all of them */
|
|
3250
3390
|
if (varMatches && varMatches.length) {
|
|
3251
3391
|
varMatches.forEach((m) => {
|
|
3252
|
-
|
|
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
|
+
}
|
|
3253
3396
|
})
|
|
3254
3397
|
}
|
|
3255
3398
|
// console.log('allowUnknownVars propertyString:', propertyString)
|
|
@@ -3470,7 +3613,17 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3470
3613
|
var hasFunc = funcRegex.exec(variableString)
|
|
3471
3614
|
// TODO finish Function handling. Need to move this down below resolver to resolve inner refs first
|
|
3472
3615
|
// console.log('hasFunc', hasFunc)
|
|
3473
|
-
|
|
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)) {
|
|
3474
3627
|
return variableString
|
|
3475
3628
|
}
|
|
3476
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}`)
|
|
@@ -55,11 +55,6 @@ async function executeTypeScriptFile(filePath, opts = {}) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Handle ES module default exports
|
|
59
|
-
if (tsFile && typeof tsFile === 'object' && 'default' in tsFile) {
|
|
60
|
-
tsFile = tsFile.default
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
return tsFile
|
|
64
59
|
}
|
|
65
60
|
|
|
@@ -117,11 +112,6 @@ function executeTypeScriptFileSync(filePath, opts = {}) {
|
|
|
117
112
|
}
|
|
118
113
|
}
|
|
119
114
|
|
|
120
|
-
// Handle ES module default exports
|
|
121
|
-
if (tsFile && typeof tsFile === 'object' && 'default' in tsFile) {
|
|
122
|
-
tsFile = tsFile.default
|
|
123
|
-
}
|
|
124
|
-
|
|
125
115
|
return tsFile
|
|
126
116
|
}
|
|
127
117
|
|