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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preprocesses config to fix malformed fallback references
|
|
3
|
+
* and escape variables inside help() filter arguments
|
|
4
|
+
*/
|
|
5
|
+
const { splitByComma } = require('../strings/splitByComma')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Preprocess config to fix malformed fallback references
|
|
9
|
+
* @param {Object} configObject - The parsed configuration object
|
|
10
|
+
* @param {RegExp} variableSyntax - The variable syntax regex to use
|
|
11
|
+
* @returns {Object} The preprocessed configuration object
|
|
12
|
+
*/
|
|
13
|
+
function preProcess(configObject, variableSyntax) {
|
|
14
|
+
// Known reference prefixes that should be wrapped in ${}
|
|
15
|
+
const refPrefixes = ['self:', 'opt:', 'env:', 'file:', 'text:', 'deep:']
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escape variables inside help() filter arguments so main resolver skips them
|
|
19
|
+
* Uses base64 encoding to preserve exact original syntax (supports custom variable syntax)
|
|
20
|
+
* @param {string} str - String potentially containing help() with variables
|
|
21
|
+
* @returns {string} String with help() variables escaped
|
|
22
|
+
*/
|
|
23
|
+
function escapeHelpVariables(str) {
|
|
24
|
+
if (typeof str !== 'string') return str
|
|
25
|
+
if (!variableSyntax) return str
|
|
26
|
+
|
|
27
|
+
// Match help('...') or help("...") containing variables
|
|
28
|
+
const helpPattern = /help\(['"]([^'"]+)['"]\)/g
|
|
29
|
+
|
|
30
|
+
return str.replace(helpPattern, (match, helpContent) => {
|
|
31
|
+
// Check if help content contains variables
|
|
32
|
+
if (!helpContent.match(variableSyntax)) return match
|
|
33
|
+
|
|
34
|
+
// Replace each variable match with base64-encoded placeholder
|
|
35
|
+
const escaped = helpContent.replace(variableSyntax, (varMatch) => {
|
|
36
|
+
const encoded = Buffer.from(varMatch).toString('base64')
|
|
37
|
+
return `__CONFIGVAR:${encoded}__`
|
|
38
|
+
})
|
|
39
|
+
return `help('${escaped}')`
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fix malformed fallback references in a string
|
|
45
|
+
* @param {string} str - String potentially containing variables
|
|
46
|
+
* @returns {string} String with fixed fallback references
|
|
47
|
+
*/
|
|
48
|
+
function fixFallbacksInString(str) {
|
|
49
|
+
if (typeof str !== 'string') return str
|
|
50
|
+
|
|
51
|
+
let result = str
|
|
52
|
+
let changed = true
|
|
53
|
+
|
|
54
|
+
// Keep iterating until no more changes (to handle nested variables)
|
|
55
|
+
while (changed) {
|
|
56
|
+
changed = false
|
|
57
|
+
|
|
58
|
+
// Find innermost ${...} blocks (ones that don't contain other ${)
|
|
59
|
+
let i = 0
|
|
60
|
+
while (i < result.length) {
|
|
61
|
+
if (result[i] === '$' && result[i + 1] === '{') {
|
|
62
|
+
const start = i
|
|
63
|
+
let braceCount = 1
|
|
64
|
+
let j = i + 2
|
|
65
|
+
|
|
66
|
+
// Find the matching closing brace by counting { and }
|
|
67
|
+
while (j < result.length && braceCount > 0) {
|
|
68
|
+
if (result[j] === '{') {
|
|
69
|
+
braceCount++
|
|
70
|
+
} else if (result[j] === '}') {
|
|
71
|
+
braceCount--
|
|
72
|
+
}
|
|
73
|
+
j++
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (braceCount === 0) {
|
|
77
|
+
const end = j
|
|
78
|
+
const match = result.substring(start, end)
|
|
79
|
+
const content = result.substring(start + 2, end - 1)
|
|
80
|
+
|
|
81
|
+
// Only process if there's a comma (indicating fallback syntax)
|
|
82
|
+
if (content.includes(',')) {
|
|
83
|
+
// Split by comma
|
|
84
|
+
const parts = splitByComma(content, variableSyntax)
|
|
85
|
+
|
|
86
|
+
if (parts.length > 1) {
|
|
87
|
+
// Check if the first part has nested ${} - if so, skip this (process inner ones first)
|
|
88
|
+
const firstPart = parts[0]
|
|
89
|
+
if (firstPart.includes('${')) {
|
|
90
|
+
i = start + 2 // Move past ${ to find inner variables
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check each part after the first (these are fallback values)
|
|
95
|
+
const fixed = parts.map((part, index) => {
|
|
96
|
+
if (index === 0) {
|
|
97
|
+
return part // Keep the main reference as-is
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const trimmed = part.trim()
|
|
101
|
+
|
|
102
|
+
// Check if this looks like a reference but is not wrapped
|
|
103
|
+
const looksLikeRef = refPrefixes.some(prefix => trimmed.startsWith(prefix))
|
|
104
|
+
const alreadyWrapped = trimmed.startsWith('${') && trimmed.endsWith('}')
|
|
105
|
+
|
|
106
|
+
if (looksLikeRef && !alreadyWrapped) {
|
|
107
|
+
return ` \${${trimmed}}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return ` ${trimmed}`
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const replacement = `\${${fixed.join(',')}}`
|
|
114
|
+
if (replacement !== match) {
|
|
115
|
+
result = result.substring(0, start) + replacement + result.substring(end)
|
|
116
|
+
changed = true
|
|
117
|
+
break // Restart search from beginning
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
i = start + 2 // Move past ${ to continue searching for nested variables
|
|
123
|
+
} else {
|
|
124
|
+
i++
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
i++
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Recursively traverse and fix the config object
|
|
137
|
+
*/
|
|
138
|
+
function traverseAndFix(obj) {
|
|
139
|
+
if (typeof obj === 'string') {
|
|
140
|
+
// First escape help() variables, then fix fallbacks
|
|
141
|
+
const withHelpEscaped = escapeHelpVariables(obj)
|
|
142
|
+
return fixFallbacksInString(withHelpEscaped)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (Array.isArray(obj)) {
|
|
146
|
+
return obj.map(item => traverseAndFix(item))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (obj !== null && typeof obj === 'object') {
|
|
150
|
+
const result = {}
|
|
151
|
+
for (const key in obj) {
|
|
152
|
+
if (obj.hasOwnProperty(key)) {
|
|
153
|
+
result[key] = traverseAndFix(obj[key])
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return obj
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return traverseAndFix(configObject)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = preProcess
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Utilities for parsing and normalizing file paths in variable references
|
|
2
|
+
|
|
3
|
+
const { splitCsv } = require('../strings/splitCsv')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a file path (add ./ prefix, fix .//, skip deep refs)
|
|
7
|
+
* @param {string} filePath - The file path to normalize
|
|
8
|
+
* @returns {string|null} Normalized path, or null if should be skipped
|
|
9
|
+
*/
|
|
10
|
+
function normalizePath(filePath) {
|
|
11
|
+
// Skip deep references
|
|
12
|
+
if (filePath.includes('deep:')) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let normalized = filePath
|
|
17
|
+
|
|
18
|
+
// Add ./ prefix for relative paths
|
|
19
|
+
if (
|
|
20
|
+
!filePath.startsWith('./') &&
|
|
21
|
+
!filePath.startsWith('../') &&
|
|
22
|
+
!filePath.startsWith('/') &&
|
|
23
|
+
!filePath.startsWith('~')
|
|
24
|
+
) {
|
|
25
|
+
normalized = './' + filePath
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fix double slashes
|
|
29
|
+
if (normalized.startsWith('.//')) {
|
|
30
|
+
normalized = normalized.replace('.//', './')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return normalized
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract file path from a file() or text() variable string
|
|
38
|
+
* @param {string} variableString - The variable string (with or without ${} wrapper)
|
|
39
|
+
* @returns {object|null} Object with filePath, or null if no match
|
|
40
|
+
*/
|
|
41
|
+
function extractFilePath(variableString) {
|
|
42
|
+
// Match both ${file(...)} and file(...) formats
|
|
43
|
+
const fileMatch = variableString.match(/^(?:\$\{)?(?:file|text)\((.*?)\)/)
|
|
44
|
+
if (!fileMatch || !fileMatch[1]) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { trimSurroundingQuotes } = require('../strings/quoteUtils')
|
|
49
|
+
const fileContent = fileMatch[1].trim()
|
|
50
|
+
const parts = splitCsv(fileContent)
|
|
51
|
+
let filePath = parts[0].trim()
|
|
52
|
+
|
|
53
|
+
// Remove quotes if present
|
|
54
|
+
filePath = trimSurroundingQuotes(filePath, false)
|
|
55
|
+
|
|
56
|
+
return { filePath }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize a file() or text() variable string
|
|
61
|
+
* Strips key accessors and normalizes the path inside
|
|
62
|
+
* @param {string} variableString - e.g. "file('./config.json'):key" or "file(config.json)"
|
|
63
|
+
* @returns {string} Normalized variable string, e.g. "file(./config.json)"
|
|
64
|
+
*/
|
|
65
|
+
function normalizeFileVariable(variableString) {
|
|
66
|
+
if (!variableString.match(/^(?:file|text)\(/)) {
|
|
67
|
+
return variableString
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Strip sub-key accessors like :topLevel, :nested.value, etc.
|
|
71
|
+
let normalized = variableString.replace(/:[\w.[\]]+$/, '')
|
|
72
|
+
|
|
73
|
+
// Normalize the path inside
|
|
74
|
+
normalized = normalized.replace(/^(file|text)\((.+?)\)/, (match, funcName, filePath) => {
|
|
75
|
+
let cleanPath = filePath.trim().replace(/^["']|["']$/g, '')
|
|
76
|
+
const normalizedPath = normalizePath(cleanPath)
|
|
77
|
+
return normalizedPath ? `${funcName}(${normalizedPath})` : match
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return normalized
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve inner variables in a string from config values
|
|
85
|
+
* @param {string} str - String containing variables like ${self:stage}
|
|
86
|
+
* @param {RegExp} variableSyntax - Regex to match variable syntax
|
|
87
|
+
* @param {object} config - Config object to look up values
|
|
88
|
+
* @param {function} getProp - Function to get nested property (e.g. dotProp.get)
|
|
89
|
+
* @returns {{resolved: string, didResolve: boolean}} Resolved string and whether resolution happened
|
|
90
|
+
*/
|
|
91
|
+
function resolveInnerVariables(str, variableSyntax, config, getProp) {
|
|
92
|
+
const varMatches = str.match(variableSyntax)
|
|
93
|
+
if (!varMatches) {
|
|
94
|
+
return { resolved: str, didResolve: false }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let canResolve = true
|
|
98
|
+
let resolved = str
|
|
99
|
+
for (const varMatch of varMatches) {
|
|
100
|
+
const innerVar = varMatch.slice(2, -1) // Remove ${ and }
|
|
101
|
+
let configPath = null
|
|
102
|
+
|
|
103
|
+
// Handle self: prefix
|
|
104
|
+
if (innerVar.startsWith('self:')) {
|
|
105
|
+
configPath = innerVar.slice(5)
|
|
106
|
+
} else if (!innerVar.includes(':')) {
|
|
107
|
+
// dot.prop style
|
|
108
|
+
configPath = innerVar
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (configPath) {
|
|
112
|
+
const configValue = getProp(config, configPath)
|
|
113
|
+
// Only use if it's a static value (not another variable)
|
|
114
|
+
if (configValue !== undefined &&
|
|
115
|
+
typeof configValue === 'string' &&
|
|
116
|
+
!configValue.match(variableSyntax)) {
|
|
117
|
+
resolved = resolved.replace(varMatch, configValue)
|
|
118
|
+
} else {
|
|
119
|
+
canResolve = false
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
canResolve = false
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { resolved: canResolve ? resolved : str, didResolve: canResolve }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
normalizePath,
|
|
133
|
+
extractFilePath,
|
|
134
|
+
normalizeFileVariable,
|
|
135
|
+
resolveInnerVariables,
|
|
136
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Tests for file path parsing and normalization utilities
|
|
2
|
+
|
|
3
|
+
const { test } = require('uvu')
|
|
4
|
+
const assert = require('uvu/assert')
|
|
5
|
+
const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVariables } = require('./filePathUtils')
|
|
6
|
+
|
|
7
|
+
// normalizePath tests
|
|
8
|
+
|
|
9
|
+
test('normalizePath - returns null for deep: references', () => {
|
|
10
|
+
assert.is(normalizePath('deep:1'), null)
|
|
11
|
+
assert.is(normalizePath('some/deep:path'), null)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('normalizePath - adds ./ prefix to bare paths', () => {
|
|
15
|
+
assert.is(normalizePath('config.json'), './config.json')
|
|
16
|
+
assert.is(normalizePath('path/to/file.yml'), './path/to/file.yml')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('normalizePath - keeps ./ paths unchanged', () => {
|
|
20
|
+
assert.is(normalizePath('./config.json'), './config.json')
|
|
21
|
+
assert.is(normalizePath('./path/to/file.yml'), './path/to/file.yml')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('normalizePath - keeps ../ paths unchanged', () => {
|
|
25
|
+
assert.is(normalizePath('../config.json'), '../config.json')
|
|
26
|
+
assert.is(normalizePath('../../path/to/file.yml'), '../../path/to/file.yml')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('normalizePath - keeps absolute paths unchanged', () => {
|
|
30
|
+
assert.is(normalizePath('/etc/config.json'), '/etc/config.json')
|
|
31
|
+
assert.is(normalizePath('/home/user/file.yml'), '/home/user/file.yml')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('normalizePath - keeps ~ paths unchanged', () => {
|
|
35
|
+
assert.is(normalizePath('~/config.json'), '~/config.json')
|
|
36
|
+
assert.is(normalizePath('~/.config/file.yml'), '~/.config/file.yml')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('normalizePath - fixes .// to ./', () => {
|
|
40
|
+
assert.is(normalizePath('.//config.json'), './config.json')
|
|
41
|
+
assert.is(normalizePath('.//path/to/file.yml'), './path/to/file.yml')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// extractFilePath tests
|
|
45
|
+
|
|
46
|
+
test('extractFilePath - extracts path from file(...) format', () => {
|
|
47
|
+
const result = extractFilePath('file(./config.json)')
|
|
48
|
+
assert.is(result.filePath, './config.json')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('extractFilePath - extracts path from ${file(...)} format', () => {
|
|
52
|
+
const result = extractFilePath('${file(./config.json)}')
|
|
53
|
+
assert.is(result.filePath, './config.json')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('extractFilePath - extracts path from text(...) format', () => {
|
|
57
|
+
const result = extractFilePath('text(./readme.txt)')
|
|
58
|
+
assert.is(result.filePath, './readme.txt')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('extractFilePath - extracts path from ${text(...)} format', () => {
|
|
62
|
+
const result = extractFilePath('${text(./readme.txt)}')
|
|
63
|
+
assert.is(result.filePath, './readme.txt')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('extractFilePath - handles single-quoted paths', () => {
|
|
67
|
+
const result = extractFilePath("file('./config.json')")
|
|
68
|
+
assert.is(result.filePath, './config.json')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('extractFilePath - handles double-quoted paths', () => {
|
|
72
|
+
const result = extractFilePath('file("./config.json")')
|
|
73
|
+
assert.is(result.filePath, './config.json')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('extractFilePath - extracts first path when fallback present', () => {
|
|
77
|
+
const result = extractFilePath("file(./env.yml, 'default')")
|
|
78
|
+
assert.is(result.filePath, './env.yml')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('extractFilePath - handles path with key accessor', () => {
|
|
82
|
+
const result = extractFilePath('file(./env.yml):FOO')
|
|
83
|
+
assert.is(result.filePath, './env.yml')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('extractFilePath - handles complex fallback with key accessor', () => {
|
|
87
|
+
const result = extractFilePath("file(./env.yml):SECRET, 'default-value'")
|
|
88
|
+
assert.is(result.filePath, './env.yml')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('extractFilePath - returns null for non-file patterns', () => {
|
|
92
|
+
assert.is(extractFilePath('opt:stage'), null)
|
|
93
|
+
assert.is(extractFilePath('${self:provider.stage}'), null)
|
|
94
|
+
assert.is(extractFilePath('env:FOO'), null)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('extractFilePath - returns null for empty input', () => {
|
|
98
|
+
assert.is(extractFilePath(''), null)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('extractFilePath - handles bare filename', () => {
|
|
102
|
+
const result = extractFilePath('file(config.json)')
|
|
103
|
+
assert.is(result.filePath, 'config.json')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// normalizeFileVariable tests
|
|
107
|
+
|
|
108
|
+
test('normalizeFileVariable - returns non-file strings unchanged', () => {
|
|
109
|
+
assert.is(normalizeFileVariable('opt:stage'), 'opt:stage')
|
|
110
|
+
assert.is(normalizeFileVariable('self:provider.stage'), 'self:provider.stage')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('normalizeFileVariable - normalizes bare path in file()', () => {
|
|
114
|
+
assert.is(normalizeFileVariable('file(config.json)'), 'file(./config.json)')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('normalizeFileVariable - normalizes bare path in text()', () => {
|
|
118
|
+
assert.is(normalizeFileVariable('text(readme.txt)'), 'text(./readme.txt)')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('normalizeFileVariable - strips key accessor', () => {
|
|
122
|
+
assert.is(normalizeFileVariable('file(./env.yml):FOO'), 'file(./env.yml)')
|
|
123
|
+
assert.is(normalizeFileVariable('file(./config.json):nested.value'), 'file(./config.json)')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('normalizeFileVariable - strips key accessor with array notation', () => {
|
|
127
|
+
assert.is(normalizeFileVariable('file(./data.json):items[0]'), 'file(./data.json)')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('normalizeFileVariable - removes quotes from path', () => {
|
|
131
|
+
assert.is(normalizeFileVariable("file('./config.json')"), 'file(./config.json)')
|
|
132
|
+
assert.is(normalizeFileVariable('file("./config.json")'), 'file(./config.json)')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('normalizeFileVariable - handles combined normalization', () => {
|
|
136
|
+
assert.is(normalizeFileVariable("file('config.json'):key"), 'file(./config.json)')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('normalizeFileVariable - keeps ./ paths unchanged', () => {
|
|
140
|
+
assert.is(normalizeFileVariable('file(./already-normalized.yml)'), 'file(./already-normalized.yml)')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('normalizeFileVariable - keeps ../ paths unchanged', () => {
|
|
144
|
+
assert.is(normalizeFileVariable('file(../parent/config.yml)'), 'file(../parent/config.yml)')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// resolveInnerVariables tests
|
|
148
|
+
|
|
149
|
+
const variableSyntax = /\$\{([^}]+)\}/g
|
|
150
|
+
const getProp = (obj, path) => path.split('.').reduce((o, k) => o && o[k], obj)
|
|
151
|
+
|
|
152
|
+
test('resolveInnerVariables - returns unchanged when no variables', () => {
|
|
153
|
+
const result = resolveInnerVariables('./config.json', variableSyntax, {}, getProp)
|
|
154
|
+
assert.is(result.resolved, './config.json')
|
|
155
|
+
assert.is(result.didResolve, false)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('resolveInnerVariables - resolves self: variables from config', () => {
|
|
159
|
+
const config = { stage: 'prod' }
|
|
160
|
+
const result = resolveInnerVariables('./database-${self:stage}.json', variableSyntax, config, getProp)
|
|
161
|
+
assert.is(result.resolved, './database-prod.json')
|
|
162
|
+
assert.is(result.didResolve, true)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('resolveInnerVariables - resolves dot.prop style variables', () => {
|
|
166
|
+
const config = { stage: 'dev' }
|
|
167
|
+
const result = resolveInnerVariables('./database-${stage}.json', variableSyntax, config, getProp)
|
|
168
|
+
assert.is(result.resolved, './database-dev.json')
|
|
169
|
+
assert.is(result.didResolve, true)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('resolveInnerVariables - resolves nested config paths', () => {
|
|
173
|
+
const config = { provider: { stage: 'test' } }
|
|
174
|
+
const result = resolveInnerVariables('./db-${self:provider.stage}.json', variableSyntax, config, getProp)
|
|
175
|
+
assert.is(result.resolved, './db-test.json')
|
|
176
|
+
assert.is(result.didResolve, true)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('resolveInnerVariables - resolves multiple variables', () => {
|
|
180
|
+
const config = { stage: 'prod', region: 'us-east-1' }
|
|
181
|
+
const result = resolveInnerVariables('./config-${self:stage}-${self:region}.json', variableSyntax, config, getProp)
|
|
182
|
+
assert.is(result.resolved, './config-prod-us-east-1.json')
|
|
183
|
+
assert.is(result.didResolve, true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('resolveInnerVariables - does not resolve if config value is a variable', () => {
|
|
187
|
+
const config = { stage: '${opt:stage}' }
|
|
188
|
+
const result = resolveInnerVariables('./database-${self:stage}.json', variableSyntax, config, getProp)
|
|
189
|
+
assert.is(result.resolved, './database-${self:stage}.json')
|
|
190
|
+
assert.is(result.didResolve, false)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('resolveInnerVariables - does not resolve if config value is undefined', () => {
|
|
194
|
+
const config = {}
|
|
195
|
+
const result = resolveInnerVariables('./database-${self:stage}.json', variableSyntax, config, getProp)
|
|
196
|
+
assert.is(result.resolved, './database-${self:stage}.json')
|
|
197
|
+
assert.is(result.didResolve, false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('resolveInnerVariables - does not resolve env: or opt: variables', () => {
|
|
201
|
+
const config = { stage: 'prod' }
|
|
202
|
+
const result = resolveInnerVariables('./database-${env:STAGE}.json', variableSyntax, config, getProp)
|
|
203
|
+
assert.is(result.resolved, './database-${env:STAGE}.json')
|
|
204
|
+
assert.is(result.didResolve, false)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('resolveInnerVariables - partial resolution fails completely', () => {
|
|
208
|
+
const config = { stage: 'prod' }
|
|
209
|
+
const result = resolveInnerVariables('./config-${self:stage}-${env:REGION}.json', variableSyntax, config, getProp)
|
|
210
|
+
assert.is(result.resolved, './config-${self:stage}-${env:REGION}.json')
|
|
211
|
+
assert.is(result.didResolve, false)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test.run()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the line number for a given key in config file contents
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find the line number where a key is defined in config file contents
|
|
7
|
+
* @param {string} keyToFind - The key to search for
|
|
8
|
+
* @param {string[]} lines - Array of file lines
|
|
9
|
+
* @param {string} fileType - File extension (e.g., '.yml', '.json', '.toml')
|
|
10
|
+
* @returns {number} Line number (1-indexed) or 0 if not found
|
|
11
|
+
*/
|
|
12
|
+
function findLineForKey(keyToFind, lines, fileType) {
|
|
13
|
+
if (!keyToFind || !lines || !lines.length) return 0
|
|
14
|
+
|
|
15
|
+
const escapedKey = keyToFind.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
16
|
+
|
|
17
|
+
const lineIdx = lines.findIndex((line) => {
|
|
18
|
+
// YAML: key: or key :
|
|
19
|
+
if (fileType === '.yml' || fileType === '.yaml') {
|
|
20
|
+
return new RegExp(`^\\s*${escapedKey}\\s*:`).test(line)
|
|
21
|
+
}
|
|
22
|
+
// TOML: key = or key=
|
|
23
|
+
if (fileType === '.toml') {
|
|
24
|
+
return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
|
|
25
|
+
}
|
|
26
|
+
// JSON: "key": or "key" :
|
|
27
|
+
if (fileType === '.json' || fileType === '.json5') {
|
|
28
|
+
return new RegExp(`"${escapedKey}"\\s*:`).test(line)
|
|
29
|
+
}
|
|
30
|
+
// INI: key = or key=
|
|
31
|
+
if (fileType === '.ini') {
|
|
32
|
+
return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
|
|
33
|
+
}
|
|
34
|
+
// JS/TS/ESM: key: or "key": or 'key': or `key`: or [`key`]:
|
|
35
|
+
if (['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(fileType)) {
|
|
36
|
+
return new RegExp(`(?:${escapedKey}|"${escapedKey}"|'${escapedKey}'|\`${escapedKey}\`|\\[\`${escapedKey}\`\\])\\s*:`).test(line)
|
|
37
|
+
}
|
|
38
|
+
// Default fallback: try YAML-style
|
|
39
|
+
return line.includes(`${keyToFind}:`)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return lineIdx !== -1 ? lineIdx + 1 : 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
findLineForKey
|
|
47
|
+
}
|
|
@@ -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()
|