configorama 0.6.12 → 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 +690 -778
- 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} +177 -15
- package/src/utils/{parse.js → parsing/parse.js} +13 -13
- package/src/utils/parsing/preProcess.js +165 -0
- package/src/utils/{filePathUtils.js → paths/filePathUtils.js} +3 -2
- 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} +177 -38
- 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/{filePathUtils.test.js → paths/filePathUtils.test.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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for findLineForKey utility
|
|
3
|
+
*/
|
|
4
|
+
const { test } = require('uvu')
|
|
5
|
+
const assert = require('uvu/assert')
|
|
6
|
+
const { findLineForKey } = require('./findLineForKey')
|
|
7
|
+
|
|
8
|
+
// YAML tests
|
|
9
|
+
test('YAML - finds key at start of line', () => {
|
|
10
|
+
const lines = ['foo: bar', 'baz: qux']
|
|
11
|
+
assert.equal(findLineForKey('foo', lines, '.yml'), 1)
|
|
12
|
+
assert.equal(findLineForKey('baz', lines, '.yml'), 2)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('YAML - finds key with indentation', () => {
|
|
16
|
+
const lines = ['root:', ' nested: value', ' other: thing']
|
|
17
|
+
assert.equal(findLineForKey('nested', lines, '.yaml'), 2)
|
|
18
|
+
assert.equal(findLineForKey('other', lines, '.yaml'), 3)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('YAML - finds key with space before colon', () => {
|
|
22
|
+
const lines = ['foo : bar']
|
|
23
|
+
assert.equal(findLineForKey('foo', lines, '.yml'), 1)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('YAML - returns 0 for missing key', () => {
|
|
27
|
+
const lines = ['foo: bar']
|
|
28
|
+
assert.equal(findLineForKey('missing', lines, '.yml'), 0)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// JSON tests
|
|
32
|
+
test('JSON - finds quoted key', () => {
|
|
33
|
+
const lines = ['{', ' "foo": "bar",', ' "baz": 123', '}']
|
|
34
|
+
assert.equal(findLineForKey('foo', lines, '.json'), 2)
|
|
35
|
+
assert.equal(findLineForKey('baz', lines, '.json'), 3)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('JSON - finds key with space before colon', () => {
|
|
39
|
+
const lines = ['{ "foo" : "bar" }']
|
|
40
|
+
assert.equal(findLineForKey('foo', lines, '.json'), 1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('JSON5 - works same as JSON', () => {
|
|
44
|
+
const lines = ['{', ' "foo": "bar"', '}']
|
|
45
|
+
assert.equal(findLineForKey('foo', lines, '.json5'), 2)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// TOML tests
|
|
49
|
+
test('TOML - finds key with equals', () => {
|
|
50
|
+
const lines = ['foo = "bar"', 'baz = 123']
|
|
51
|
+
assert.equal(findLineForKey('foo', lines, '.toml'), 1)
|
|
52
|
+
assert.equal(findLineForKey('baz', lines, '.toml'), 2)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('TOML - finds key without spaces around equals', () => {
|
|
56
|
+
const lines = ['foo="bar"']
|
|
57
|
+
assert.equal(findLineForKey('foo', lines, '.toml'), 1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// INI tests
|
|
61
|
+
test('INI - finds key with equals', () => {
|
|
62
|
+
const lines = ['[section]', 'foo = bar', 'baz = qux']
|
|
63
|
+
assert.equal(findLineForKey('foo', lines, '.ini'), 2)
|
|
64
|
+
assert.equal(findLineForKey('baz', lines, '.ini'), 3)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// JS/TS tests
|
|
68
|
+
test('JS - finds unquoted key', () => {
|
|
69
|
+
const lines = ['module.exports = {', ' foo: "bar",', ' baz: 123', '}']
|
|
70
|
+
assert.equal(findLineForKey('foo', lines, '.js'), 2)
|
|
71
|
+
assert.equal(findLineForKey('baz', lines, '.js'), 3)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('JS - finds double-quoted key', () => {
|
|
75
|
+
const lines = ['module.exports = {', ' "foo": "bar"', '}']
|
|
76
|
+
assert.equal(findLineForKey('foo', lines, '.js'), 2)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('JS - finds single-quoted key', () => {
|
|
80
|
+
const lines = ["module.exports = {", " 'foo': 'bar'", "}"]
|
|
81
|
+
assert.equal(findLineForKey('foo', lines, '.js'), 2)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('JS - finds backtick-quoted key', () => {
|
|
85
|
+
const lines = ['module.exports = {', ' `foo`: "bar"', '}']
|
|
86
|
+
assert.equal(findLineForKey('foo', lines, '.js'), 2)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('TS - works same as JS', () => {
|
|
90
|
+
const lines = ['export default {', ' foo: "bar"', '}']
|
|
91
|
+
assert.equal(findLineForKey('foo', lines, '.ts'), 2)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('MJS - works same as JS', () => {
|
|
95
|
+
const lines = ['export default {', ' foo: "bar"', '}']
|
|
96
|
+
assert.equal(findLineForKey('foo', lines, '.mjs'), 2)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Edge cases
|
|
100
|
+
test('returns 0 for empty lines array', () => {
|
|
101
|
+
assert.equal(findLineForKey('foo', [], '.yml'), 0)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('returns 0 for null/undefined key', () => {
|
|
105
|
+
const lines = ['foo: bar']
|
|
106
|
+
assert.equal(findLineForKey(null, lines, '.yml'), 0)
|
|
107
|
+
assert.equal(findLineForKey(undefined, lines, '.yml'), 0)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('returns 0 for null/undefined lines', () => {
|
|
111
|
+
assert.equal(findLineForKey('foo', null, '.yml'), 0)
|
|
112
|
+
assert.equal(findLineForKey('foo', undefined, '.yml'), 0)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('escapes special regex characters in key', () => {
|
|
116
|
+
const lines = ['foo.bar: value', 'baz[0]: thing']
|
|
117
|
+
assert.equal(findLineForKey('foo.bar', lines, '.yml'), 1)
|
|
118
|
+
assert.equal(findLineForKey('baz[0]', lines, '.yml'), 2)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('unknown file type falls back to YAML-style', () => {
|
|
122
|
+
const lines = ['foo: bar']
|
|
123
|
+
assert.equal(findLineForKey('foo', lines, '.unknown'), 1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test.run()
|
|
@@ -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
|
+
}
|