configorama 0.6.11 → 0.6.13
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 +196 -24
- package/cli.js +3 -3
- package/package.json +1 -1
- package/src/index.js +22 -32
- package/src/main.js +775 -857
- package/src/parsers/yaml.js +3 -47
- package/src/resolvers/valueFromCron.js +3 -1
- package/src/resolvers/valueFromEnv.js +1 -0
- package/src/resolvers/valueFromEval.js +1 -0
- package/src/resolvers/valueFromFile.js +394 -0
- package/src/resolvers/valueFromGit.js +3 -2
- package/src/resolvers/valueFromOptions.js +1 -0
- package/src/resolvers/valueFromString.js +2 -1
- package/src/sync.js +12 -5
- package/src/utils/parsing/arrayToJsonPath.test.js +56 -0
- package/src/utils/{enrichMetadata.js → parsing/enrichMetadata.js} +244 -94
- package/src/utils/{parse.js → parsing/parse.js} +13 -13
- package/src/utils/parsing/preProcess.js +165 -0
- package/src/utils/paths/filePathUtils.js +136 -0
- package/src/utils/paths/filePathUtils.test.js +214 -0
- package/src/utils/paths/findLineForKey.js +47 -0
- package/src/utils/paths/findLineForKey.test.js +126 -0
- package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +22 -26
- package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
- package/src/utils/regex/index.js +23 -1
- package/src/utils/resolution/preResolveVariable.js +260 -0
- package/src/utils/resolution/preResolveVariable.test.js +98 -0
- package/src/utils/strings/bracketMatcher.js +86 -0
- package/src/utils/strings/bracketMatcher.test.js +135 -0
- package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
- package/src/utils/strings/formatFunctionArgs.test.js +77 -0
- package/src/utils/strings/quoteUtils.js +89 -0
- package/src/utils/strings/quoteUtils.test.js +217 -0
- package/src/utils/strings/replaceAll.test.js +82 -0
- package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
- package/src/utils/strings/splitCsv.js +38 -0
- package/src/utils/strings/splitCsv.test.js +96 -0
- package/src/utils/strings/textUtils.test.js +86 -0
- package/src/utils/{configWizard.js → ui/configWizard.js} +212 -60
- package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
- package/src/utils/{logs.js → ui/logs.js} +3 -3
- package/src/utils/validation/isValidValue.test.js +64 -0
- package/src/utils/validation/warnIfNotFound.js +52 -0
- package/src/utils/variables/appendDeepVariable.test.js +41 -0
- package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
- package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
- package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
- package/src/utils/variables/getVariableType.test.js +109 -0
- package/src/utils/variables/variableUtils.test.js +117 -0
- package/src/utils/isValidValue.js +0 -8
- package/src/utils/splitCsv.js +0 -29
- package/src/utils/trimSurroundingQuotes.js +0 -5
- /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
- /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
- /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.js} +0 -0
- /package/src/utils/{find-project-root.js → paths/findProjectRoot.js} +0 -0
- /package/src/utils/{resolveAlias.test.js → paths/resolveAlias.test.js} +0 -0
- /package/src/utils/{replaceAll.js → strings/replaceAll.js} +0 -0
- /package/src/utils/{splitByComma.test.js → strings/splitByComma.test.js} +0 -0
- /package/src/utils/{textUtils.js → strings/textUtils.js} +0 -0
- /package/src/utils/{chalk.js → ui/chalk.js} +0 -0
- /package/src/utils/{deep-log.js → ui/deep-log.js} +0 -0
- /package/src/utils/{appendDeepVariable.js → variables/appendDeepVariable.js} +0 -0
- /package/src/utils/{cleanVariable.test.js → variables/cleanVariable.test.js} +0 -0
- /package/src/utils/{getVariableType.js → variables/getVariableType.js} +0 -0
- /package/src/utils/{variableUtils.js → variables/variableUtils.js} +0 -0
|
@@ -2,28 +2,39 @@ const os = require('os')
|
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const findUp = require('find-up')
|
|
5
|
-
const trimSurroundingQuotes = require('
|
|
5
|
+
const { trimSurroundingQuotes } = require('../strings/quoteUtils')
|
|
6
6
|
const { resolveAlias } = require('./resolveAlias')
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Core path resolution logic - resolves a file path handling absolute paths, symlinks, and findUp
|
|
10
|
+
* @param {string} pathToResolve - The path to resolve
|
|
11
|
+
* @param {string} basePath - The base directory for relative paths
|
|
12
|
+
* @returns {string} The resolved full file path
|
|
13
|
+
*/
|
|
14
|
+
function resolveFilePath(pathToResolve, basePath) {
|
|
15
|
+
let fullFilePath = path.isAbsolute(pathToResolve) ? pathToResolve : path.join(basePath, pathToResolve)
|
|
13
16
|
|
|
14
17
|
if (fs.existsSync(fullFilePath)) {
|
|
15
18
|
// Get real path to handle potential symlinks (but don't fatal error)
|
|
16
19
|
fullFilePath = fs.realpathSync(fullFilePath)
|
|
17
|
-
|
|
18
20
|
// Only match files that are relative
|
|
19
|
-
} else if (
|
|
20
|
-
const cleanName = path.basename(
|
|
21
|
-
|
|
21
|
+
} else if (pathToResolve.match(/\.\//)) {
|
|
22
|
+
const cleanName = path.basename(pathToResolve)
|
|
23
|
+
const findUpResult = findUp.sync(cleanName, { cwd: basePath })
|
|
24
|
+
if (findUpResult) {
|
|
25
|
+
fullFilePath = findUpResult
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
return fullFilePath
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
module.exports = function getFullPath(fileString, cwd) {
|
|
33
|
+
const configPath = cwd || process.cwd()
|
|
34
|
+
const relativePath = fileString.replace('~', os.homedir())
|
|
35
|
+
return resolveFilePath(relativePath, configPath)
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
/**
|
|
28
39
|
* Resolves a file path from a matched file string (e.g., from file() or text() syntax)
|
|
29
40
|
* @param {string} matchedFileString - The matched file string (e.g., "file(path/to/file.js)")
|
|
@@ -38,22 +49,7 @@ function resolveFilePathFromMatch(matchedFileString, syntax, configPath) {
|
|
|
38
49
|
|
|
39
50
|
// Resolve alias if the path contains alias syntax
|
|
40
51
|
const resolvedPath = resolveAlias(relativePath, configPath)
|
|
41
|
-
|
|
42
|
-
let fullFilePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.join(configPath, resolvedPath)
|
|
43
|
-
|
|
44
|
-
if (fs.existsSync(fullFilePath)) {
|
|
45
|
-
// Get real path to handle potential symlinks (but don't fatal error)
|
|
46
|
-
fullFilePath = fs.realpathSync(fullFilePath)
|
|
47
|
-
|
|
48
|
-
// Only match files that are relative
|
|
49
|
-
} else if (resolvedPath.match(/\.\//)) {
|
|
50
|
-
// TODO test higher parent refs
|
|
51
|
-
const cleanName = path.basename(resolvedPath)
|
|
52
|
-
const findUpResult = findUp.sync(cleanName, { cwd: configPath })
|
|
53
|
-
if (findUpResult) {
|
|
54
|
-
fullFilePath = findUpResult
|
|
55
|
-
}
|
|
56
|
-
}
|
|
52
|
+
const fullFilePath = resolveFilePath(resolvedPath, configPath)
|
|
57
53
|
|
|
58
54
|
return { fullFilePath, resolvedPath, relativePath }
|
|
59
55
|
}
|
package/src/utils/regex/index.js
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared regex patterns and utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const funcRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
|
|
6
|
+
const funcStartOfLineRegex = /^(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
|
|
7
|
+
const subFunctionRegex = /(\w+):(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Combine multiple regex patterns into single OR pattern
|
|
11
|
+
* @param {RegExp[]} regexes - Array of regex patterns to combine
|
|
12
|
+
* @returns {RegExp} Combined regex with OR operator
|
|
13
|
+
*/
|
|
14
|
+
function combineRegexes(regexes) {
|
|
15
|
+
const patterns = regexes.map(regex => regex.source).filter(Boolean)
|
|
16
|
+
return new RegExp(`(${patterns.join('|')})`)
|
|
17
|
+
}
|
|
1
18
|
|
|
2
19
|
module.exports = {
|
|
3
|
-
|
|
20
|
+
funcRegex,
|
|
21
|
+
funcStartOfLineRegex,
|
|
22
|
+
subFunctionRegex,
|
|
23
|
+
combineRegexes,
|
|
24
|
+
// Keep old export name for backwards compat
|
|
25
|
+
functionRegex: funcRegex
|
|
4
26
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-resolve variables that don't have dynamic dependencies.
|
|
3
|
+
* Uses existing resolvers to avoid duplicating logic.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const dotProp = require('dot-prop')
|
|
8
|
+
|
|
9
|
+
const createGitResolver = require('../../resolvers/valueFromGit')
|
|
10
|
+
const { parseFileContents } = require('../../resolvers/valueFromFile')
|
|
11
|
+
|
|
12
|
+
// Cache for resolved values (they don't change during execution)
|
|
13
|
+
const resolverCache = {}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a string contains unresolved variable syntax
|
|
17
|
+
* @param {string} str - String to check
|
|
18
|
+
* @param {RegExp} variableSyntax - Variable syntax regex
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function hasUnresolvedVars(str, variableSyntax) {
|
|
22
|
+
if (typeof str !== 'string') return false
|
|
23
|
+
if (!variableSyntax) {
|
|
24
|
+
// Fallback to basic pattern if no syntax provided
|
|
25
|
+
return /\$\{[^}]+\}/.test(str)
|
|
26
|
+
}
|
|
27
|
+
// Create a new regex from the source to avoid stateful lastIndex issues
|
|
28
|
+
const regex = new RegExp(variableSyntax.source, variableSyntax.flags.replace('g', ''))
|
|
29
|
+
return regex.test(str)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pre-resolve a single variable reference
|
|
34
|
+
* @param {string} varString - Variable string like "self:path.to.value" or "env:VAR_NAME"
|
|
35
|
+
* @param {object} context - Resolution context
|
|
36
|
+
* @param {object} context.config - Original config object
|
|
37
|
+
* @param {string} context.configDir - Config file directory
|
|
38
|
+
* @param {RegExp} context.variableSyntax - Variable syntax regex
|
|
39
|
+
* @returns {Promise<*>} Resolved value or undefined if can't pre-resolve
|
|
40
|
+
*/
|
|
41
|
+
async function preResolveSingle(varString, context) {
|
|
42
|
+
const { config = {}, configDir, variableSyntax } = context
|
|
43
|
+
|
|
44
|
+
// self: reference
|
|
45
|
+
if (varString.startsWith('self:')) {
|
|
46
|
+
const path = varString.slice(5).trim()
|
|
47
|
+
if (hasUnresolvedVars(path, variableSyntax)) return undefined
|
|
48
|
+
const value = dotProp.get(config, path)
|
|
49
|
+
// Only return if the value itself doesn't contain variables
|
|
50
|
+
if (value !== undefined && !hasUnresolvedVars(JSON.stringify(value), variableSyntax)) {
|
|
51
|
+
return value
|
|
52
|
+
}
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// env: reference
|
|
57
|
+
if (varString.startsWith('env:')) {
|
|
58
|
+
const envVar = varString.slice(4).trim()
|
|
59
|
+
if (hasUnresolvedVars(envVar, variableSyntax)) return undefined
|
|
60
|
+
return process.env[envVar]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// git: reference - use existing resolver
|
|
64
|
+
if (varString.startsWith('git:')) {
|
|
65
|
+
const gitVar = varString.slice(4).trim()
|
|
66
|
+
if (hasUnresolvedVars(gitVar, variableSyntax)) return undefined
|
|
67
|
+
|
|
68
|
+
const cacheKey = `git:${configDir || '.'}:${gitVar}`
|
|
69
|
+
if (resolverCache[cacheKey] !== undefined) {
|
|
70
|
+
return resolverCache[cacheKey]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const gitResolver = createGitResolver(configDir)
|
|
75
|
+
const value = await gitResolver.resolver(`git:${gitVar}`)
|
|
76
|
+
resolverCache[cacheKey] = value
|
|
77
|
+
return value
|
|
78
|
+
} catch (e) {
|
|
79
|
+
resolverCache[cacheKey] = undefined
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// opt: reference - CLI options
|
|
85
|
+
if (varString.startsWith('opt:')) {
|
|
86
|
+
const optName = varString.slice(4).trim()
|
|
87
|
+
if (hasUnresolvedVars(optName, variableSyntax)) return undefined
|
|
88
|
+
const { options = {} } = context
|
|
89
|
+
return options[optName]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// file() reference - read file contents
|
|
93
|
+
const fileMatch = varString.match(/^file\((.+?)\)(?::(.+))?$/)
|
|
94
|
+
if (fileMatch) {
|
|
95
|
+
const filePath = fileMatch[1].trim()
|
|
96
|
+
const subPath = fileMatch[2] ? fileMatch[2].trim() : null
|
|
97
|
+
|
|
98
|
+
if (hasUnresolvedVars(filePath, variableSyntax)) return undefined
|
|
99
|
+
|
|
100
|
+
const cacheKey = `file:${configDir || '.'}:${filePath}`
|
|
101
|
+
if (resolverCache[cacheKey] !== undefined) {
|
|
102
|
+
const cached = resolverCache[cacheKey]
|
|
103
|
+
if (subPath && cached && typeof cached === 'object') {
|
|
104
|
+
return dotProp.get(cached, subPath)
|
|
105
|
+
}
|
|
106
|
+
return cached
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const resolvedPath = path.resolve(configDir || '.', filePath)
|
|
111
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
112
|
+
resolverCache[cacheKey] = undefined
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const content = fs.readFileSync(resolvedPath, 'utf8')
|
|
117
|
+
const parsed = parseFileContents(content, resolvedPath)
|
|
118
|
+
|
|
119
|
+
resolverCache[cacheKey] = parsed
|
|
120
|
+
|
|
121
|
+
if (subPath && parsed && typeof parsed === 'object') {
|
|
122
|
+
return dotProp.get(parsed, subPath)
|
|
123
|
+
}
|
|
124
|
+
return parsed
|
|
125
|
+
} catch (e) {
|
|
126
|
+
resolverCache[cacheKey] = undefined
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Simple config reference (no prefix, just a path like "foo.bar")
|
|
132
|
+
// Only if it looks like a valid path (alphanumeric, dots, underscores)
|
|
133
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(varString)) {
|
|
134
|
+
const value = dotProp.get(config, varString)
|
|
135
|
+
if (value !== undefined && !hasUnresolvedVars(JSON.stringify(value), variableSyntax)) {
|
|
136
|
+
return value
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pre-resolve variable references in a string
|
|
145
|
+
* Replaces ${varRef} patterns with resolved values where possible
|
|
146
|
+
* @param {string} str - String containing variable references
|
|
147
|
+
* @param {object} context - Resolution context
|
|
148
|
+
* @param {object} context.config - Original config object
|
|
149
|
+
* @param {string} context.configDir - Config file directory
|
|
150
|
+
* @param {RegExp} context.variableSyntax - Variable syntax regex
|
|
151
|
+
* @param {object} [options] - Options
|
|
152
|
+
* @param {boolean} [options.formatArrays=true] - Join arrays with ", "
|
|
153
|
+
* @returns {Promise<string>} String with pre-resolved variables
|
|
154
|
+
*/
|
|
155
|
+
async function preResolveString(str, context, options = {}) {
|
|
156
|
+
if (typeof str !== 'string') return str
|
|
157
|
+
|
|
158
|
+
const { formatArrays = true } = options
|
|
159
|
+
const { variableSyntax } = context
|
|
160
|
+
|
|
161
|
+
// Use provided syntax or fallback to basic pattern
|
|
162
|
+
const varPattern = variableSyntax ? new RegExp(variableSyntax.source, 'g') : /\$\{([^{}]+)\}/g
|
|
163
|
+
|
|
164
|
+
// Collect all matches first since we need async resolution
|
|
165
|
+
const matches = []
|
|
166
|
+
let match
|
|
167
|
+
while ((match = varPattern.exec(str)) !== null) {
|
|
168
|
+
matches.push({ match: match[0], index: match.index })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (matches.length === 0) return str
|
|
172
|
+
|
|
173
|
+
// Resolve all matches
|
|
174
|
+
const resolutions = await Promise.all(matches.map(async ({ match: matchStr }) => {
|
|
175
|
+
// Extract content between ${ and }
|
|
176
|
+
const varContent = matchStr.slice(2, -1)
|
|
177
|
+
|
|
178
|
+
// Skip if the content itself has ${} (nested variable)
|
|
179
|
+
if (hasUnresolvedVars(varContent, variableSyntax)) {
|
|
180
|
+
return matchStr
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Handle fallback syntax: ${var, fallback1, fallback2}
|
|
184
|
+
// Try each in order until one resolves
|
|
185
|
+
const parts = varContent.split(',').map(p => p.trim())
|
|
186
|
+
|
|
187
|
+
for (const part of parts) {
|
|
188
|
+
// Skip quoted literals for now (they're fallbacks, not resolvable)
|
|
189
|
+
if (/^['"].*['"]$/.test(part)) {
|
|
190
|
+
// Return the literal without quotes
|
|
191
|
+
return part.slice(1, -1)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Skip numeric literals
|
|
195
|
+
if (/^\d+(\.\d+)?$/.test(part)) {
|
|
196
|
+
return part
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const resolved = await preResolveSingle(part, context)
|
|
200
|
+
if (resolved !== undefined) {
|
|
201
|
+
// Format the value
|
|
202
|
+
if (Array.isArray(resolved) && formatArrays) {
|
|
203
|
+
return resolved.join(', ')
|
|
204
|
+
}
|
|
205
|
+
if (typeof resolved === 'object') {
|
|
206
|
+
return JSON.stringify(resolved)
|
|
207
|
+
}
|
|
208
|
+
return String(resolved)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Couldn't resolve any part, return original
|
|
213
|
+
return matchStr
|
|
214
|
+
}))
|
|
215
|
+
|
|
216
|
+
// Replace matches with resolved values (in reverse order to preserve indices)
|
|
217
|
+
let result = str
|
|
218
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
219
|
+
const { match: matchStr, index } = matches[i]
|
|
220
|
+
result = result.slice(0, index) + resolutions[i] + result.slice(index + matchStr.length)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return result
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Pre-resolve variables in an object recursively
|
|
228
|
+
* @param {*} obj - Object to process
|
|
229
|
+
* @param {object} context - Resolution context
|
|
230
|
+
* @param {object} [options] - Options passed to preResolveString
|
|
231
|
+
* @returns {Promise<*>} Object with pre-resolved variables
|
|
232
|
+
*/
|
|
233
|
+
async function preResolveObject(obj, context, options = {}) {
|
|
234
|
+
if (typeof obj === 'string') {
|
|
235
|
+
return preResolveString(obj, context, options)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (Array.isArray(obj)) {
|
|
239
|
+
return Promise.all(obj.map(item => preResolveObject(item, context, options)))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (obj !== null && typeof obj === 'object') {
|
|
243
|
+
const result = {}
|
|
244
|
+
const entries = Object.keys(obj)
|
|
245
|
+
const values = await Promise.all(entries.map(key => preResolveObject(obj[key], context, options)))
|
|
246
|
+
entries.forEach((key, i) => {
|
|
247
|
+
result[key] = values[i]
|
|
248
|
+
})
|
|
249
|
+
return result
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return obj
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
preResolveSingle,
|
|
257
|
+
preResolveString,
|
|
258
|
+
preResolveObject,
|
|
259
|
+
hasUnresolvedVars
|
|
260
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for preResolveVariable utility
|
|
3
|
+
*/
|
|
4
|
+
const { test } = require('uvu')
|
|
5
|
+
const assert = require('uvu/assert')
|
|
6
|
+
const { preResolveSingle, preResolveString, hasUnresolvedVars } = require('./preResolveVariable')
|
|
7
|
+
|
|
8
|
+
// Default variable syntax (matches configorama default)
|
|
9
|
+
const variableSyntax = /\$\{([\s\S]+?)\}/g
|
|
10
|
+
|
|
11
|
+
// Test hasUnresolvedVars
|
|
12
|
+
test('hasUnresolvedVars - detects variable syntax', () => {
|
|
13
|
+
assert.is(hasUnresolvedVars('${foo}', variableSyntax), true)
|
|
14
|
+
assert.is(hasUnresolvedVars('${self:bar}', variableSyntax), true)
|
|
15
|
+
assert.is(hasUnresolvedVars('hello ${world}', variableSyntax), true)
|
|
16
|
+
assert.is(hasUnresolvedVars('no variables here', variableSyntax), false)
|
|
17
|
+
assert.is(hasUnresolvedVars('', variableSyntax), false)
|
|
18
|
+
assert.is(hasUnresolvedVars(null, variableSyntax), false)
|
|
19
|
+
assert.is(hasUnresolvedVars(123, variableSyntax), false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Test preResolveSingle with config refs
|
|
23
|
+
test('preResolveSingle - resolves simple config path', async () => {
|
|
24
|
+
const config = { foo: 'bar', nested: { value: 42 } }
|
|
25
|
+
const ctx = { config, variableSyntax }
|
|
26
|
+
assert.is(await preResolveSingle('foo', ctx), 'bar')
|
|
27
|
+
assert.is(await preResolveSingle('nested.value', ctx), 42)
|
|
28
|
+
assert.is(await preResolveSingle('notfound', ctx), undefined)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('preResolveSingle - resolves self: refs', async () => {
|
|
32
|
+
const config = { appName: 'MyApp', settings: { port: 3000 } }
|
|
33
|
+
const ctx = { config, variableSyntax }
|
|
34
|
+
assert.is(await preResolveSingle('self:appName', ctx), 'MyApp')
|
|
35
|
+
assert.is(await preResolveSingle('self:settings.port', ctx), 3000)
|
|
36
|
+
assert.is(await preResolveSingle('self:missing', ctx), undefined)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('preResolveSingle - resolves env: refs', async () => {
|
|
40
|
+
process.env.TEST_PRE_RESOLVE = 'testvalue'
|
|
41
|
+
const ctx = { config: {}, variableSyntax }
|
|
42
|
+
assert.is(await preResolveSingle('env:TEST_PRE_RESOLVE', ctx), 'testvalue')
|
|
43
|
+
assert.is(await preResolveSingle('env:DEFINITELY_NOT_SET_12345', ctx), undefined)
|
|
44
|
+
delete process.env.TEST_PRE_RESOLVE
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('preResolveSingle - skips values with unresolved vars', async () => {
|
|
48
|
+
const config = { dynamic: '${other}' }
|
|
49
|
+
const ctx = { config, variableSyntax }
|
|
50
|
+
assert.is(await preResolveSingle('dynamic', ctx), undefined)
|
|
51
|
+
assert.is(await preResolveSingle('self:dynamic', ctx), undefined)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Test preResolveString
|
|
55
|
+
test('preResolveString - resolves variables in string', async () => {
|
|
56
|
+
const config = { name: 'World', count: 5 }
|
|
57
|
+
const ctx = { config, variableSyntax }
|
|
58
|
+
const result = await preResolveString('Hello ${name}, count: ${count}', ctx)
|
|
59
|
+
assert.is(result, 'Hello World, count: 5')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('preResolveString - formats arrays as comma-separated', async () => {
|
|
63
|
+
const config = { items: ['a', 'b', 'c'] }
|
|
64
|
+
const ctx = { config, variableSyntax }
|
|
65
|
+
const result = await preResolveString('Items: ${items}', ctx)
|
|
66
|
+
assert.is(result, 'Items: a, b, c')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('preResolveString - handles fallback values', async () => {
|
|
70
|
+
const config = { existing: 'found' }
|
|
71
|
+
const ctx = { config, variableSyntax }
|
|
72
|
+
assert.is(await preResolveString('${existing, "default"}', ctx), 'found')
|
|
73
|
+
assert.is(await preResolveString('${missing, "default"}', ctx), 'default')
|
|
74
|
+
assert.is(await preResolveString('${missing, 42}', ctx), '42')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('preResolveString - leaves unresolvable vars unchanged', async () => {
|
|
78
|
+
const ctx = { config: {}, variableSyntax }
|
|
79
|
+
const result = await preResolveString('Value: ${unknown}', ctx)
|
|
80
|
+
assert.is(result, 'Value: ${unknown}')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('preResolveString - handles env: in strings', async () => {
|
|
84
|
+
process.env.TEST_STRING_RESOLVE = 'envval'
|
|
85
|
+
const ctx = { config: {}, variableSyntax }
|
|
86
|
+
const result = await preResolveString('Env: ${env:TEST_STRING_RESOLVE}', ctx)
|
|
87
|
+
assert.is(result, 'Env: envval')
|
|
88
|
+
delete process.env.TEST_STRING_RESOLVE
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('preResolveString - handles self: in strings', async () => {
|
|
92
|
+
const config = { version: '1.0.0' }
|
|
93
|
+
const ctx = { config, variableSyntax }
|
|
94
|
+
const result = await preResolveString('Version: ${self:version}', ctx)
|
|
95
|
+
assert.is(result, 'Version: 1.0.0')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test.run()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds all outermost matching brace pairs in a string
|
|
3
|
+
* @param {string} text - The text to search
|
|
4
|
+
* @param {string} openChar - The opening character (default: '{')
|
|
5
|
+
* @param {string} closeChar - The closing character (default: '}')
|
|
6
|
+
* @param {string} prefix - Optional prefix before opening char (e.g., '$' for '${')
|
|
7
|
+
* @returns {Array<string>} Array of matched substrings including delimiters
|
|
8
|
+
*/
|
|
9
|
+
function findOutermostBraces(text, openChar = '{', closeChar = '}', prefix = '') {
|
|
10
|
+
const matches = []
|
|
11
|
+
let i = 0
|
|
12
|
+
const openPattern = prefix + openChar
|
|
13
|
+
|
|
14
|
+
while (i < text.length) {
|
|
15
|
+
// Check if we have a match at this position
|
|
16
|
+
const checkLen = openPattern.length
|
|
17
|
+
if (text.substring(i, i + checkLen) === openPattern) {
|
|
18
|
+
let depth = 1
|
|
19
|
+
let start = i
|
|
20
|
+
i += checkLen
|
|
21
|
+
|
|
22
|
+
while (i < text.length && depth > 0) {
|
|
23
|
+
if (text[i] === openChar) {
|
|
24
|
+
depth++
|
|
25
|
+
} else if (text[i] === closeChar) {
|
|
26
|
+
depth--
|
|
27
|
+
}
|
|
28
|
+
i++
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (depth === 0) {
|
|
32
|
+
matches.push(text.substring(start, i))
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
i++
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return matches
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Alternative implementation for finding outermost braces using depth tracking
|
|
44
|
+
* Optimized for simple bracket matching without prefix
|
|
45
|
+
* @param {string} text - The text to search
|
|
46
|
+
* @param {string} openChar - The opening character
|
|
47
|
+
* @param {string} closeChar - The closing character
|
|
48
|
+
* @returns {Array<string>} Array of matched substrings including delimiters
|
|
49
|
+
*/
|
|
50
|
+
function findOutermostBracesDepthFirst(text, openChar = '{', closeChar = '}') {
|
|
51
|
+
const results = []
|
|
52
|
+
let depth = 0
|
|
53
|
+
let startIndex = -1
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < text.length; i++) {
|
|
56
|
+
if (text[i] === openChar) {
|
|
57
|
+
if (depth === 0) {
|
|
58
|
+
startIndex = i
|
|
59
|
+
}
|
|
60
|
+
depth++
|
|
61
|
+
} else if (text[i] === closeChar) {
|
|
62
|
+
depth--
|
|
63
|
+
if (depth === 0 && startIndex !== -1) {
|
|
64
|
+
results.push(text.substring(startIndex, i + 1))
|
|
65
|
+
startIndex = -1
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return results
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Finds outermost variables with ${} syntax
|
|
75
|
+
* @param {string} text - The text to search
|
|
76
|
+
* @returns {Array<string>} Array of matched variables including ${}
|
|
77
|
+
*/
|
|
78
|
+
function findOutermostVariables(text) {
|
|
79
|
+
return findOutermostBraces(text, '{', '}', '$')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
findOutermostBraces,
|
|
84
|
+
findOutermostBracesDepthFirst,
|
|
85
|
+
findOutermostVariables
|
|
86
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const {
|
|
4
|
+
findOutermostBraces,
|
|
5
|
+
findOutermostBracesDepthFirst,
|
|
6
|
+
findOutermostVariables
|
|
7
|
+
} = require('./bracketMatcher')
|
|
8
|
+
|
|
9
|
+
// Tests for findOutermostBraces
|
|
10
|
+
test('findOutermostBraces - should find simple braces', () => {
|
|
11
|
+
const result = findOutermostBraces('text {content} more')
|
|
12
|
+
assert.equal(result, ['{content}'])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('findOutermostBraces - should find multiple brace pairs', () => {
|
|
16
|
+
const result = findOutermostBraces('{first} and {second}')
|
|
17
|
+
assert.equal(result, ['{first}', '{second}'])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('findOutermostBraces - should handle nested braces', () => {
|
|
21
|
+
const result = findOutermostBraces('{outer {inner} content}')
|
|
22
|
+
assert.equal(result, ['{outer {inner} content}'])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('findOutermostBraces - should find with prefix', () => {
|
|
26
|
+
const result = findOutermostBraces('text ${variable} more', '{', '}', '$')
|
|
27
|
+
assert.equal(result, ['${variable}'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('findOutermostBraces - should handle multiple nested levels with prefix', () => {
|
|
31
|
+
const result = findOutermostBraces('${outer ${inner ${deepest}}}', '{', '}', '$')
|
|
32
|
+
assert.equal(result, ['${outer ${inner ${deepest}}}'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('findOutermostBraces - should return empty array when no matches', () => {
|
|
36
|
+
const result = findOutermostBraces('no braces here')
|
|
37
|
+
assert.equal(result, [])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('findOutermostBraces - should handle custom delimiters', () => {
|
|
41
|
+
const result = findOutermostBraces('text [content] more', '[', ']')
|
|
42
|
+
assert.equal(result, ['[content]'])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('findOutermostBraces - should not match without prefix when prefix is specified', () => {
|
|
46
|
+
const result = findOutermostBraces('{no prefix} ${with prefix}', '{', '}', '$')
|
|
47
|
+
assert.equal(result, ['${with prefix}'])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Tests for findOutermostBracesDepthFirst
|
|
51
|
+
test('findOutermostBracesDepthFirst - should find simple braces', () => {
|
|
52
|
+
const result = findOutermostBracesDepthFirst('text {content} more')
|
|
53
|
+
assert.equal(result, ['{content}'])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('findOutermostBracesDepthFirst - should find multiple brace pairs', () => {
|
|
57
|
+
const result = findOutermostBracesDepthFirst('{first} and {second}')
|
|
58
|
+
assert.equal(result, ['{first}', '{second}'])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('findOutermostBracesDepthFirst - should handle nested braces', () => {
|
|
62
|
+
const result = findOutermostBracesDepthFirst('{outer {inner} content}')
|
|
63
|
+
assert.equal(result, ['{outer {inner} content}'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('findOutermostBracesDepthFirst - should return empty array when no matches', () => {
|
|
67
|
+
const result = findOutermostBracesDepthFirst('no braces here')
|
|
68
|
+
assert.equal(result, [])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('findOutermostBracesDepthFirst - should handle custom delimiters', () => {
|
|
72
|
+
const result = findOutermostBracesDepthFirst('text [content] more', '[', ']')
|
|
73
|
+
assert.equal(result, ['[content]'])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('findOutermostBracesDepthFirst - should handle unmatched braces gracefully', () => {
|
|
77
|
+
const result = findOutermostBracesDepthFirst('{opened but not closed')
|
|
78
|
+
assert.equal(result, [])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('findOutermostBracesDepthFirst - should handle complex nested structures', () => {
|
|
82
|
+
const result = findOutermostBracesDepthFirst('{a {b {c}}} {d {e}}')
|
|
83
|
+
assert.equal(result, ['{a {b {c}}}', '{d {e}}'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Tests for findOutermostVariables
|
|
87
|
+
test('findOutermostVariables - should find simple variable', () => {
|
|
88
|
+
const result = findOutermostVariables('text ${variable} more')
|
|
89
|
+
assert.equal(result, ['${variable}'])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('findOutermostVariables - should find multiple variables', () => {
|
|
93
|
+
const result = findOutermostVariables('${first} and ${second}')
|
|
94
|
+
assert.equal(result, ['${first}', '${second}'])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('findOutermostVariables - should handle nested variables', () => {
|
|
98
|
+
const result = findOutermostVariables('${outer ${inner}}')
|
|
99
|
+
assert.equal(result, ['${outer ${inner}}'])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('findOutermostVariables - should return empty array when no variables', () => {
|
|
103
|
+
const result = findOutermostVariables('no variables here')
|
|
104
|
+
assert.equal(result, [])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('findOutermostVariables - should ignore plain braces without dollar sign', () => {
|
|
108
|
+
const result = findOutermostVariables('{not a variable} ${is a variable}')
|
|
109
|
+
assert.equal(result, ['${is a variable}'])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('findOutermostVariables - should handle real-world serverless variables', () => {
|
|
113
|
+
const result = findOutermostVariables('${param:xyz}')
|
|
114
|
+
assert.equal(result, ['${param:xyz}'])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('findOutermostVariables - should handle deeply nested serverless variables', () => {
|
|
118
|
+
const result = findOutermostVariables('${opt:stage, ${env:foo}}')
|
|
119
|
+
assert.equal(result, ['${opt:stage, ${env:foo}}'])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('findOutermostVariables - should find multiple variables in array context', () => {
|
|
123
|
+
const text = "y: !Not [!Equals [!Join ['', ${param:xyz}]]]"
|
|
124
|
+
const result = findOutermostVariables(text)
|
|
125
|
+
assert.equal(result, ['${param:xyz}'])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('findOutermostVariables - should handle variables in YAML object context', () => {
|
|
129
|
+
const text = 'key: { value: ${self:config}, other: ${env:var} }'
|
|
130
|
+
const result = findOutermostVariables(text)
|
|
131
|
+
assert.equal(result, ['${self:config}', '${env:var}'])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Run all tests
|
|
135
|
+
test.run()
|