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
package/src/parsers/yaml.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const YAML = require('js-yaml')
|
|
2
2
|
const TOML = require('./toml')
|
|
3
3
|
const JSON = require('./json5')
|
|
4
|
+
const { findOutermostVariables, findOutermostBracesDepthFirst } = require('../utils/strings/bracketMatcher')
|
|
4
5
|
|
|
5
6
|
// Loader for custom CF syntax
|
|
6
7
|
function load(contents, options) {
|
|
@@ -57,53 +58,8 @@ function toJson(ymlContents) {
|
|
|
57
58
|
return json
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
let matches = [];
|
|
63
|
-
let depth = 0;
|
|
64
|
-
let startIndex = -1;
|
|
65
|
-
|
|
66
|
-
for (let i = 0; i < text.length; i++) {
|
|
67
|
-
if (text[i] === '$' && text[i + 1] === '{') {
|
|
68
|
-
if (depth === 0) {
|
|
69
|
-
startIndex = i;
|
|
70
|
-
}
|
|
71
|
-
depth++;
|
|
72
|
-
i++; // Skip '{'
|
|
73
|
-
} else if (text[i] === '}') {
|
|
74
|
-
depth--;
|
|
75
|
-
if (depth === 0 && startIndex !== -1) {
|
|
76
|
-
matches.push(text.substring(startIndex, i + 1));
|
|
77
|
-
startIndex = -1;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return matches;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
function matchOutermostBraces(text) {
|
|
86
|
-
let depth = 0
|
|
87
|
-
let startIndex = -1
|
|
88
|
-
let results = []
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < text.length; i++) {
|
|
91
|
-
if (text[i] === '{') {
|
|
92
|
-
if (depth === 0) {
|
|
93
|
-
startIndex = i
|
|
94
|
-
}
|
|
95
|
-
depth++
|
|
96
|
-
} else if (text[i] === '}') {
|
|
97
|
-
depth--
|
|
98
|
-
if (depth === 0 && startIndex !== -1) {
|
|
99
|
-
results.push(text.substring(startIndex, i + 1))
|
|
100
|
-
startIndex = -1
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return results
|
|
106
|
-
}
|
|
61
|
+
// Alias for backward compatibility
|
|
62
|
+
const matchOutermostBraces = findOutermostBracesDepthFirst
|
|
107
63
|
|
|
108
64
|
|
|
109
65
|
// https://regex101.com/r/XIltbc/1
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { trimSurroundingQuotes } = require('../utils/strings/quoteUtils')
|
|
1
2
|
const cronRefSyntax = RegExp(/^cron\((~?[\{\}\:\$a-zA-Z0-9._\-\/,'"\*\` ]+?)?\)/g)
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -226,7 +227,7 @@ Examples:
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
// Remove surrounding quotes if present
|
|
229
|
-
const cleanExpression = cronExpression
|
|
230
|
+
const cleanExpression = trimSurroundingQuotes(cronExpression, true)
|
|
230
231
|
|
|
231
232
|
// If already a cron expression, return it
|
|
232
233
|
if (cleanExpression.match(/^[\*\/,\-\d]+$/)) {
|
|
@@ -244,6 +245,7 @@ Examples:
|
|
|
244
245
|
|
|
245
246
|
module.exports = {
|
|
246
247
|
type: 'cron',
|
|
248
|
+
source: 'readonly',
|
|
247
249
|
prefix: 'cron',
|
|
248
250
|
syntax: '${cron(expression)}',
|
|
249
251
|
description: 'Resolves cron expressions. Examples: ${cron("every 5 minutes"}, ${cron("weekdays")}, ${cron("at 9:30")}',
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves values from file references (file() and text() syntax)
|
|
3
|
+
*/
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const { trim } = require('../utils/lodash')
|
|
6
|
+
const { splitCsv } = require('../utils/strings/splitCsv')
|
|
7
|
+
const { resolveFilePathFromMatch } = require('../utils/paths/getFullFilePath')
|
|
8
|
+
const { findNestedVariables } = require('../utils/variables/findNestedVariables')
|
|
9
|
+
const { makeBox } = require('@davidwells/box-logger')
|
|
10
|
+
const { encodeJsSyntax } = require('../utils/encoders/js-fixes')
|
|
11
|
+
|
|
12
|
+
/* File Parsers */
|
|
13
|
+
const YAML = require('../parsers/yaml')
|
|
14
|
+
const TOML = require('../parsers/toml')
|
|
15
|
+
const INI = require('../parsers/ini')
|
|
16
|
+
const JSON5 = require('../parsers/json5')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse file contents based on file extension
|
|
20
|
+
* @param {string} content - Raw file contents
|
|
21
|
+
* @param {string} filePath - File path (used to determine extension)
|
|
22
|
+
* @returns {*} Parsed content
|
|
23
|
+
*/
|
|
24
|
+
function parseFileContents(content, filePath) {
|
|
25
|
+
const ext = filePath.split('.').pop().toLowerCase()
|
|
26
|
+
|
|
27
|
+
if (ext === 'json' || ext === 'json5') {
|
|
28
|
+
return JSON5.parse(content)
|
|
29
|
+
}
|
|
30
|
+
if (ext === 'yml' || ext === 'yaml') {
|
|
31
|
+
return YAML.parse(content)
|
|
32
|
+
}
|
|
33
|
+
if (ext === 'toml' || ext === 'tml') {
|
|
34
|
+
return TOML.parse(content)
|
|
35
|
+
}
|
|
36
|
+
if (ext === 'ini') {
|
|
37
|
+
return INI.parse(content)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Return raw content for other files
|
|
41
|
+
return content
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolves a value from a file reference
|
|
46
|
+
* @param {object} ctx - Context object with instance properties
|
|
47
|
+
* @param {string} ctx.configPath - Base path for file resolution
|
|
48
|
+
* @param {Array} ctx.fileRefsFound - Mutable array tracking file refs
|
|
49
|
+
* @param {RegExp} ctx.variableSyntax - Regex for variable syntax
|
|
50
|
+
* @param {object} ctx.variablesKnownTypes - Known variable types
|
|
51
|
+
* @param {object} ctx.variableTypes - Variable types
|
|
52
|
+
* @param {object} ctx.opts - Options object
|
|
53
|
+
* @param {object} ctx.originalConfig - Original config
|
|
54
|
+
* @param {object} ctx.config - Current config
|
|
55
|
+
* @param {Function} ctx.getDeeperValue - Method for nested lookups
|
|
56
|
+
* @param {RegExp} ctx.fileRefSyntax - Regex for file() syntax
|
|
57
|
+
* @param {RegExp} ctx.textRefSyntax - Regex for text() syntax
|
|
58
|
+
* @param {string} variableString - The variable string to resolve
|
|
59
|
+
* @param {object} options - Resolution options
|
|
60
|
+
* @returns {Promise<any>}
|
|
61
|
+
*/
|
|
62
|
+
async function getValueFromFile(ctx, variableString, options) {
|
|
63
|
+
const opts = options || {}
|
|
64
|
+
const syntax = opts.asRawText ? ctx.textRefSyntax : ctx.fileRefSyntax
|
|
65
|
+
// console.log('From file', `"${variableString}"`)
|
|
66
|
+
let matchedFileString = variableString.match(syntax)[0]
|
|
67
|
+
// console.log('matchedFileString', matchedFileString)
|
|
68
|
+
|
|
69
|
+
// Get function input params if any supplied https://regex101.com/r/qlNFVm/1
|
|
70
|
+
// var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
|
|
71
|
+
var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
|
|
72
|
+
// tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
|
|
73
|
+
var hasParams = funcParamsRegex.exec(matchedFileString)
|
|
74
|
+
|
|
75
|
+
let argsToPass = []
|
|
76
|
+
if (hasParams) {
|
|
77
|
+
const splitter = splitCsv(hasParams[2])
|
|
78
|
+
const argsFound = splitter.map((arg) => {
|
|
79
|
+
const cleanArg = trim(arg).replace(/^'|"/, '').replace(/'|"$/, '')
|
|
80
|
+
return cleanArg
|
|
81
|
+
})
|
|
82
|
+
// console.log('argsFound', argsFound)
|
|
83
|
+
|
|
84
|
+
// If function has more arguments than file path
|
|
85
|
+
if (argsFound.length && argsFound.length > 1) {
|
|
86
|
+
matchedFileString = argsFound[0]
|
|
87
|
+
argsToPass = argsFound.filter((arg, i) => {
|
|
88
|
+
return i !== 0
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// console.log('argsToPass', argsToPass)
|
|
93
|
+
|
|
94
|
+
const fileDetails = resolveFilePathFromMatch(matchedFileString, syntax, ctx.configPath)
|
|
95
|
+
// console.log('fileDetails', fileDetails)
|
|
96
|
+
|
|
97
|
+
const { fullFilePath, resolvedPath, relativePath } = fileDetails
|
|
98
|
+
|
|
99
|
+
const exists = fs.existsSync(fullFilePath)
|
|
100
|
+
|
|
101
|
+
ctx.fileRefsFound.push({
|
|
102
|
+
// location: options.context.path.join('.'),
|
|
103
|
+
filePath: fullFilePath,
|
|
104
|
+
relativePath,
|
|
105
|
+
resolvedVariableString: options.context.value,
|
|
106
|
+
originalVariableString: options.context.originalSource,
|
|
107
|
+
containsVariables: options.context.value !== options.context.originalSource,
|
|
108
|
+
exists,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
let fileExtension = resolvedPath.split('.')
|
|
112
|
+
|
|
113
|
+
fileExtension = fileExtension[fileExtension.length - 1].toLowerCase()
|
|
114
|
+
|
|
115
|
+
// Validate file exists
|
|
116
|
+
if (!exists) {
|
|
117
|
+
const originalVar = options.context && options.context.originalSource
|
|
118
|
+
|
|
119
|
+
const findNestedResult = findNestedVariables(
|
|
120
|
+
originalVar,
|
|
121
|
+
ctx.variableSyntax,
|
|
122
|
+
ctx.variablesKnownTypes,
|
|
123
|
+
options.context.path,
|
|
124
|
+
ctx.variableTypes
|
|
125
|
+
)
|
|
126
|
+
// console.log('findNestedResult', findNestedResult)
|
|
127
|
+
let hasFallback = false
|
|
128
|
+
if (findNestedResult) {
|
|
129
|
+
const varDetails = findNestedResult[0]
|
|
130
|
+
// console.log('varDetails', varDetails)
|
|
131
|
+
hasFallback = varDetails.hasFallback
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// check if original var has fallback value
|
|
135
|
+
// console.log('NO FILE FOUND', fullFilePath)
|
|
136
|
+
// console.log('variableString', variableString)
|
|
137
|
+
|
|
138
|
+
if (!hasFallback && !ctx.opts.allowUnknownFileRefs) {
|
|
139
|
+
const errorMsg = makeBox({
|
|
140
|
+
title: `File Not Found in ${originalVar}`,
|
|
141
|
+
minWidth: '100%',
|
|
142
|
+
text: `Variable ${variableString} cannot resolve due to missing file.
|
|
143
|
+
|
|
144
|
+
File not found ${fullFilePath}
|
|
145
|
+
|
|
146
|
+
Default fallback value will be used if provided.
|
|
147
|
+
|
|
148
|
+
${JSON.stringify(options.context, null, 2)}`,
|
|
149
|
+
})
|
|
150
|
+
console.log(errorMsg)
|
|
151
|
+
}
|
|
152
|
+
// TODO maybe reject. YAML does not allow for null/undefined values
|
|
153
|
+
// return Promise.reject(new Error(errorMsg))
|
|
154
|
+
return Promise.resolve(undefined)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let valueToPopulate
|
|
158
|
+
|
|
159
|
+
const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
|
|
160
|
+
|
|
161
|
+
/* handle case for referencing raw JS files to inline them */
|
|
162
|
+
if (argsToPass.length
|
|
163
|
+
&& (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw')
|
|
164
|
+
|| opts.asRawText
|
|
165
|
+
) {
|
|
166
|
+
// Encode foo() to foo__PH_PAREN_OPEN__) to avoid function collisions
|
|
167
|
+
valueToPopulate = encodeJsSyntax(variableFileContents)
|
|
168
|
+
return Promise.resolve(valueToPopulate)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Build context for executable files
|
|
172
|
+
const valueForFunction = {
|
|
173
|
+
originalConfig: ctx.originalConfig,
|
|
174
|
+
config: ctx.config,
|
|
175
|
+
opts: ctx.opts,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Process JS files
|
|
179
|
+
if (fileExtension === 'js' || fileExtension === 'cjs') {
|
|
180
|
+
const jsFile = require(fullFilePath)
|
|
181
|
+
const { moduleName } = parseModuleReference(variableString, matchedFileString)
|
|
182
|
+
const returnValueFunction = moduleName ? jsFile[moduleName] : jsFile
|
|
183
|
+
|
|
184
|
+
return processExecutableFile({
|
|
185
|
+
fileModule: jsFile,
|
|
186
|
+
returnValueFunction,
|
|
187
|
+
valueForFunction,
|
|
188
|
+
argsToPass,
|
|
189
|
+
variableString,
|
|
190
|
+
matchedFileString,
|
|
191
|
+
relativePath,
|
|
192
|
+
fileType: 'javascript',
|
|
193
|
+
getDeeperValue: ctx.getDeeperValue
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (fileExtension === 'ts' || fileExtension === 'tsx' || fileExtension === 'mts' || fileExtension === 'cts') {
|
|
198
|
+
const { executeTypeScriptFile } = require('../parsers/typescript')
|
|
199
|
+
const { moduleName } = parseModuleReference(variableString, matchedFileString)
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const tsFile = await executeTypeScriptFile(fullFilePath, { dynamicArgs: () => argsToPass })
|
|
203
|
+
let returnValueFunction = tsFile.config || tsFile.default || tsFile
|
|
204
|
+
if (moduleName) {
|
|
205
|
+
returnValueFunction = tsFile[moduleName]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return processExecutableFile({
|
|
209
|
+
fileModule: tsFile,
|
|
210
|
+
returnValueFunction,
|
|
211
|
+
valueForFunction,
|
|
212
|
+
argsToPass,
|
|
213
|
+
variableString,
|
|
214
|
+
matchedFileString,
|
|
215
|
+
relativePath,
|
|
216
|
+
fileType: 'TypeScript',
|
|
217
|
+
getDeeperValue: ctx.getDeeperValue
|
|
218
|
+
})
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return Promise.reject(new Error(`Error processing TypeScript file: ${err.message}`))
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (fileExtension === 'mjs' || fileExtension === 'esm') {
|
|
225
|
+
const { executeESMFile } = require('../parsers/esm')
|
|
226
|
+
const { moduleName } = parseModuleReference(variableString, matchedFileString)
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const esmFile = await executeESMFile(fullFilePath, { dynamicArgs: () => argsToPass })
|
|
230
|
+
let returnValueFunction = esmFile.config || esmFile.default || esmFile
|
|
231
|
+
if (moduleName) {
|
|
232
|
+
returnValueFunction = esmFile[moduleName]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return processExecutableFile({
|
|
236
|
+
fileModule: esmFile,
|
|
237
|
+
returnValueFunction,
|
|
238
|
+
valueForFunction,
|
|
239
|
+
argsToPass,
|
|
240
|
+
variableString,
|
|
241
|
+
matchedFileString,
|
|
242
|
+
relativePath,
|
|
243
|
+
fileType: 'ESM',
|
|
244
|
+
getDeeperValue: ctx.getDeeperValue
|
|
245
|
+
})
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return Promise.reject(new Error(`Error processing ESM file: ${err.message}`))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Process everything except JS, TS, and ESM
|
|
252
|
+
if (fileExtension !== 'js' && fileExtension !== 'ts' && fileExtension !== 'mjs' && fileExtension !== 'esm') {
|
|
253
|
+
/* Read initial file */
|
|
254
|
+
valueToPopulate = variableFileContents
|
|
255
|
+
|
|
256
|
+
// File reference has :subKey lookup. Must dig deeper
|
|
257
|
+
if (matchedFileString !== variableString) {
|
|
258
|
+
if (fileExtension === 'yml' || fileExtension === 'yaml') {
|
|
259
|
+
valueToPopulate = JSON.stringify(YAML.parse(valueToPopulate))
|
|
260
|
+
}
|
|
261
|
+
if (fileExtension === 'toml' || fileExtension === 'tml') {
|
|
262
|
+
valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
|
|
263
|
+
}
|
|
264
|
+
if (fileExtension === 'ini') {
|
|
265
|
+
valueToPopulate = INI.toJson(valueToPopulate)
|
|
266
|
+
}
|
|
267
|
+
// console.log('deep', variableString)
|
|
268
|
+
// console.log('matchedFileString', matchedFileString)
|
|
269
|
+
let deepProperties = variableString.replace(matchedFileString, '')
|
|
270
|
+
// Support both : and . as the separator for sub properties
|
|
271
|
+
const firstChar = deepProperties.substring(0, 1)
|
|
272
|
+
if (firstChar !== ':' && firstChar !== '.') {
|
|
273
|
+
const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
|
|
274
|
+
Please use ":" or "." to reference sub properties. ${deepProperties}`
|
|
275
|
+
return Promise.reject(new Error(errorMessage))
|
|
276
|
+
}
|
|
277
|
+
deepProperties = deepProperties.slice(1).split('.')
|
|
278
|
+
return ctx.getDeeperValue(deepProperties, valueToPopulate)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (fileExtension === 'yml' || fileExtension === 'yaml') {
|
|
282
|
+
valueToPopulate = YAML.parse(valueToPopulate)
|
|
283
|
+
return Promise.resolve(valueToPopulate)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (fileExtension === 'toml' || fileExtension === 'tml') {
|
|
287
|
+
valueToPopulate = TOML.parse(valueToPopulate)
|
|
288
|
+
return Promise.resolve(valueToPopulate)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (fileExtension === 'ini') {
|
|
292
|
+
valueToPopulate = INI.parse(valueToPopulate)
|
|
293
|
+
return Promise.resolve(valueToPopulate)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (fileExtension === 'json' || fileExtension === 'json5') {
|
|
297
|
+
valueToPopulate = JSON5.parse(valueToPopulate)
|
|
298
|
+
return Promise.resolve(valueToPopulate)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// console.log('fall thru', valueToPopulate)
|
|
302
|
+
return Promise.resolve(valueToPopulate)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Parses variable string to extract module reference path
|
|
307
|
+
* Supports both : and . as separators for module references
|
|
308
|
+
* @param {string} variableString - The full variable string
|
|
309
|
+
* @param {string} matchedFileString - The matched file path portion
|
|
310
|
+
* @returns {{ variableArray: string[], moduleName: string|null }}
|
|
311
|
+
*/
|
|
312
|
+
function parseModuleReference(variableString, matchedFileString) {
|
|
313
|
+
let variableArray = variableString.split(':')
|
|
314
|
+
if (variableArray.length === 1) {
|
|
315
|
+
const dotIndex = variableString.indexOf(matchedFileString) + matchedFileString.length
|
|
316
|
+
const afterMatch = variableString.substring(dotIndex)
|
|
317
|
+
if (afterMatch.startsWith('.')) {
|
|
318
|
+
variableArray = [variableString.substring(0, dotIndex), afterMatch.substring(1)]
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let moduleName = null
|
|
323
|
+
if (variableArray[1]) {
|
|
324
|
+
moduleName = variableArray[1].split('.')[0]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { variableArray, moduleName }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Extracts deep properties from variable string after file match
|
|
332
|
+
* @param {string} variableString - The full variable string
|
|
333
|
+
* @param {string} matchedFileString - The matched file path portion
|
|
334
|
+
* @returns {string[]} Array of property keys to traverse
|
|
335
|
+
*/
|
|
336
|
+
function extractDeepProperties(variableString, matchedFileString) {
|
|
337
|
+
let deepProperties = variableString.replace(matchedFileString, '')
|
|
338
|
+
deepProperties = deepProperties.slice(1).split('.')
|
|
339
|
+
deepProperties.splice(0, 1)
|
|
340
|
+
return deepProperties.map((prop) => trim(prop))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Processes executable file (JS/TS/ESM) and resolves deep properties
|
|
345
|
+
* @param {object} params - Parameters
|
|
346
|
+
* @param {object} params.fileModule - The loaded module
|
|
347
|
+
* @param {Function} params.returnValueFunction - The function to call
|
|
348
|
+
* @param {object} params.valueForFunction - Context passed to the function
|
|
349
|
+
* @param {string[]} params.argsToPass - Additional args for the function
|
|
350
|
+
* @param {string} params.variableString - Original variable string
|
|
351
|
+
* @param {string} params.matchedFileString - Matched file path
|
|
352
|
+
* @param {string} params.relativePath - Relative file path for errors
|
|
353
|
+
* @param {string} params.fileType - Type of file (javascript/TypeScript/ESM)
|
|
354
|
+
* @param {Function} params.getDeeperValue - Function to resolve nested values
|
|
355
|
+
* @returns {Promise<any>}
|
|
356
|
+
*/
|
|
357
|
+
async function processExecutableFile({
|
|
358
|
+
fileModule,
|
|
359
|
+
returnValueFunction,
|
|
360
|
+
valueForFunction,
|
|
361
|
+
argsToPass,
|
|
362
|
+
variableString,
|
|
363
|
+
matchedFileString,
|
|
364
|
+
relativePath,
|
|
365
|
+
fileType,
|
|
366
|
+
getDeeperValue
|
|
367
|
+
}) {
|
|
368
|
+
if (typeof returnValueFunction !== 'function') {
|
|
369
|
+
const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
|
|
370
|
+
Check if your ${fileType} is exporting a function that returns a value.`
|
|
371
|
+
return Promise.reject(new Error(errorMessage))
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const valueToPopulate = returnValueFunction.call(fileModule, valueForFunction, ...argsToPass)
|
|
375
|
+
|
|
376
|
+
return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
|
|
377
|
+
const deepProperties = extractDeepProperties(variableString, matchedFileString)
|
|
378
|
+
return getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
|
|
379
|
+
if (typeof deepValueToPopulateResolved === 'undefined') {
|
|
380
|
+
const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
|
|
381
|
+
Check if your ${fileType} is returning the correct data.`
|
|
382
|
+
return Promise.reject(new Error(errorMessage))
|
|
383
|
+
}
|
|
384
|
+
return Promise.resolve(deepValueToPopulateResolved)
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = {
|
|
390
|
+
getValueFromFile,
|
|
391
|
+
parseFileContents,
|
|
392
|
+
parseModuleReference,
|
|
393
|
+
extractDeepProperties
|
|
394
|
+
}
|
|
@@ -5,8 +5,8 @@ const path = require('path')
|
|
|
5
5
|
const childProcess = require('child_process')
|
|
6
6
|
const GitUrlParse = require('git-url-parse')
|
|
7
7
|
const { functionRegex } = require('../utils/regex')
|
|
8
|
-
const formatFunctionArgs = require('../utils/formatFunctionArgs')
|
|
9
|
-
const { findProjectRoot } = require('../utils/
|
|
8
|
+
const formatFunctionArgs = require('../utils/strings/formatFunctionArgs')
|
|
9
|
+
const { findProjectRoot } = require('../utils/paths/findProjectRoot')
|
|
10
10
|
const GIT_PREFIX = 'git'
|
|
11
11
|
const gitVariableSyntax = RegExp(/^git:/g)
|
|
12
12
|
|
|
@@ -330,6 +330,7 @@ async function getGitRemote(name = 'origin') {
|
|
|
330
330
|
module.exports = function createGitResolver(cwd) {
|
|
331
331
|
return {
|
|
332
332
|
type: 'git',
|
|
333
|
+
source: 'readonly',
|
|
333
334
|
prefix: 'git',
|
|
334
335
|
syntax: '${git:valueType}',
|
|
335
336
|
description: `Resolves Git variables. Available valueTypes: ${Object.values(GIT_KEYS).join(', ')}`,
|
|
@@ -14,6 +14,7 @@ function getValueFromOptions(variableString, options) {
|
|
|
14
14
|
|
|
15
15
|
module.exports = {
|
|
16
16
|
type: 'options',
|
|
17
|
+
source: 'user',
|
|
17
18
|
prefix: 'opt',
|
|
18
19
|
syntax: '${opt:flagName}',
|
|
19
20
|
description: 'Resolves CLI option flags. Examples: ${opt:stage}, ${opt:other, "fallbackValue"}',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
const { trimSurroundingQuotes } = require('../utils/strings/quoteUtils')
|
|
1
2
|
|
|
2
3
|
const stringRefSyntax = RegExp(/(?:('|").*?\1)/g)
|
|
3
4
|
|
|
4
5
|
function getValueFromString(variableString) {
|
|
5
|
-
const valueToPopulate = variableString
|
|
6
|
+
const valueToPopulate = trimSurroundingQuotes(variableString, false)
|
|
6
7
|
return Promise.resolve(valueToPopulate)
|
|
7
8
|
}
|
|
8
9
|
|
package/src/sync.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const Configorama = require('./main')
|
|
4
|
-
const getFullPath = require('./utils/getFullFilePath')
|
|
5
|
-
const enrichMetadata = require('./utils/enrichMetadata')
|
|
4
|
+
const getFullPath = require('./utils/paths/getFullFilePath')
|
|
5
|
+
const enrichMetadata = require('./utils/parsing/enrichMetadata')
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Force synchronous invocation of async API
|
|
@@ -54,19 +54,26 @@ module.exports = function configoramaSync(variableSources = []) {
|
|
|
54
54
|
const metadata = instance.collectVariableMetadata()
|
|
55
55
|
|
|
56
56
|
// Enrich metadata with resolution tracking data collected during execution
|
|
57
|
-
const enrichedMetadata = enrichMetadata(
|
|
57
|
+
const enrichedMetadata = await enrichMetadata(
|
|
58
58
|
metadata,
|
|
59
59
|
instance.resolutionTracking,
|
|
60
60
|
instance.variableSyntax,
|
|
61
61
|
instance.fileRefsFound,
|
|
62
62
|
instance.originalConfig,
|
|
63
63
|
instance.configFilePath,
|
|
64
|
-
Object.keys(instance.filters)
|
|
64
|
+
Object.keys(instance.filters),
|
|
65
|
+
result, // pass resolved config for post-resolution enrichment
|
|
66
|
+
options,
|
|
67
|
+
instance.variableTypes
|
|
65
68
|
)
|
|
66
69
|
|
|
67
70
|
return {
|
|
71
|
+
variableSyntax: instance.variableSyntax,
|
|
72
|
+
variableTypes: instance.variableTypes,
|
|
68
73
|
config: result,
|
|
69
|
-
|
|
74
|
+
originalConfig: instance.originalConfig,
|
|
75
|
+
metadata: enrichedMetadata,
|
|
76
|
+
resolutionHistory: enrichedMetadata.resolutionHistory,
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const { arrayToJsonPath } = require('./arrayToJsonPath')
|
|
4
|
+
|
|
5
|
+
test('arrayToJsonPath - should handle single string element', () => {
|
|
6
|
+
const result = arrayToJsonPath(['root'])
|
|
7
|
+
assert.equal(result, 'root')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('arrayToJsonPath - should join string elements with dots', () => {
|
|
11
|
+
const result = arrayToJsonPath(['root', 'child', 'grandchild'])
|
|
12
|
+
assert.equal(result, 'root.child.grandchild')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('arrayToJsonPath - should handle numeric indices with brackets', () => {
|
|
16
|
+
const result = arrayToJsonPath(['array', 0, 'property'])
|
|
17
|
+
assert.equal(result, 'array[0].property')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('arrayToJsonPath - should handle mixed string and number paths', () => {
|
|
21
|
+
const result = arrayToJsonPath(['users', 5, 'name'])
|
|
22
|
+
assert.equal(result, 'users[5].name')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('arrayToJsonPath - should handle consecutive numbers', () => {
|
|
26
|
+
const result = arrayToJsonPath(['matrix', 0, 1])
|
|
27
|
+
assert.equal(result, 'matrix[0][1]')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('arrayToJsonPath - should handle single number', () => {
|
|
31
|
+
const result = arrayToJsonPath([0])
|
|
32
|
+
assert.equal(result, '0')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('arrayToJsonPath - should handle complex nested path', () => {
|
|
36
|
+
const result = arrayToJsonPath(['data', 'items', 3, 'values', 2, 'name'])
|
|
37
|
+
assert.equal(result, 'data.items[3].values[2].name')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('arrayToJsonPath - should handle empty array', () => {
|
|
41
|
+
const result = arrayToJsonPath([])
|
|
42
|
+
assert.equal(result, '')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('arrayToJsonPath - should convert number to string for first element', () => {
|
|
46
|
+
const result = arrayToJsonPath([123, 'property'])
|
|
47
|
+
assert.equal(result, '123.property')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('arrayToJsonPath - should handle property names with special chars', () => {
|
|
51
|
+
const result = arrayToJsonPath(['root', 'my-prop', 'my_prop'])
|
|
52
|
+
assert.equal(result, 'root.my-prop.my_prop')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Run all tests
|
|
56
|
+
test.run()
|